feat: add plugin system (#1034)

* feat: add plugin system

* fix db docker build

* fix hammerhead readme, add strava subscription news to docs

* fixes and sdk improvements

* fix: reduce Meilisearch load, debounce federation sync (#1012)

* optimize meili trail index

* several fixes

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Bump svelte from 5.55.5 to 5.56.0 in /docs (#1032)

Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.55.5 to 5.56.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.56.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Release v0.19.2 (#1035)

* chore: release v0.19.2

* add changelog

---------

Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>

* speed up plugin sync and several small fixes

* concepts for security improvements and process stability

* improve concept

* security concept implemented

* remove insecure TLS

* worker concept implemented

* fixes and cleanup

* fixes

* docu

* mermaid, namings

* WASM plugin host improvements, plugin logging

* fix db migration

* Improve plugin config and category mapping UI

* fixes

* further fixes

* remove manual test sync

* fix db migration and strava mapping

* type added, UI improvements

* fix plugin card toggle clickable area

* optimize synch status card layout

* plugin type 'trails' instead of 'integration'

* session auth validation in UI

* fix komoot date and waypoints

* improve category mapping

* fix send to hammerhead: trail name

* plugin setup error handling improved

* fix review findings

* re-mapping added

* rename remote_category

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
This commit is contained in:
slothful-vassal
2026-06-22 15:00:44 +02:00
committed by GitHub
parent 2e3d8537b8
commit 485ec53f6d
196 changed files with 20947 additions and 5454 deletions

View File

@@ -0,0 +1,536 @@
package migrations
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/types"
)
func init() {
m.Register(func(app core.App) error {
// Create plugin_instances collection
jsonData := `{
"createRule": "@request.auth.id = user.id",
"deleteRule": "@request.auth.id = user.id",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text430001001",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"hidden": false,
"id": "relation430001002",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text430001003",
"max": 64,
"min": 1,
"name": "plugin_id",
"pattern": "^[a-z0-9][a-z0-9_-]*$",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "bool430001004",
"name": "enabled",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
},
{
"hidden": false,
"id": "json430001005",
"maxSize": 2000000,
"name": "auth",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json430001006",
"maxSize": 2000000,
"name": "config",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json430001007",
"maxSize": 2000000,
"name": "state",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "select430001008",
"maxSelect": 1,
"name": "status",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": [
"configured",
"needs_auth",
"needs_reauth",
"syncing",
"rate_limited",
"unavailable",
"unsupported_protocol",
"error",
"disabled"
]
},
{
"hidden": false,
"id": "json430001009",
"maxSize": 2000000,
"name": "last_error",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "date430001010",
"max": "",
"min": "",
"name": "last_sync_at",
"presentable": false,
"required": false,
"system": false,
"type": "date"
},
{
"hidden": false,
"id": "date430001011",
"max": "",
"min": "",
"name": "retry_not_before",
"presentable": false,
"required": false,
"system": false,
"type": "date"
},
{
"hidden": false,
"id": "autodate430001012",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate430001013",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_430001000",
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_plugin_instances_user_plugin_id` + "`" + ` ON ` + "`" + `plugin_instances` + "`" + ` (` + "`" + `user` + "`" + `, ` + "`" + `plugin_id` + "`" + `)"
],
"listRule": "@request.auth.id = user.id",
"name": "plugin_instances",
"system": false,
"type": "base",
"updateRule": "@request.auth.id = user.id",
"viewRule": "@request.auth.id = user.id"
}`
if _, err := app.FindCollectionByNameOrId("pbc_430001000"); err != nil {
collection := &core.Collection{}
if err := json.Unmarshal([]byte(jsonData), collection); err != nil {
return err
}
if err := app.Save(collection); err != nil {
return err
}
}
if err := migrateLegacyIntegrationsToPluginInstances(app); err != nil {
return err
}
// Remove the previous hard-coded provider settings collection after
// migrating its configuration into plugin_instances. The migration is
// data-only and does not require the corresponding plugin bundles to be
// installed.
if legacyCollection, err := app.FindCollectionByNameOrId("integrations"); err == nil {
if err := app.Delete(legacyCollection); err != nil {
return err
}
}
// Add user field to trail_external_reference and update index to be user-scoped
refCollection, err := app.FindCollectionByNameOrId("trail_external_reference")
if err != nil {
return err
}
if refCollection.Fields.GetByName("user") == nil {
if err := refCollection.Fields.AddMarshaledJSONAt(2, []byte(`{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"hidden": false,
"id": "relation430002001",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
// Replace the global unique (provider, external_id) index with a
// user-scoped one so the same external trail can be imported by
// multiple users. This must be managed via the collection metadata
// (not a raw DROP INDEX), otherwise app.Save would recreate the old
// index from the still-present metadata entry.
keptIndexes := refCollection.Indexes[:0]
for _, idx := range refCollection.Indexes {
if strings.Contains(idx, "idx_trail_external_reference_provider_external_id") {
continue
}
keptIndexes = append(keptIndexes, idx)
}
refCollection.Indexes = append(keptIndexes,
"CREATE UNIQUE INDEX `idx_trail_external_reference_user_provider_external_id` ON `trail_external_reference` (`user`, `provider`, `external_id`)",
)
if err := app.Save(refCollection); err != nil {
return err
}
refs, err := app.FindAllRecords("trail_external_reference")
if err != nil {
return err
}
for _, ref := range refs {
trailID := ref.GetString("trail")
if trailID == "" {
continue
}
trail, err := app.FindRecordById("trails", trailID)
if err != nil {
continue
}
actor, err := app.FindRecordById("activitypub_actors", trail.GetString("author"))
if err != nil {
continue
}
userID := actor.GetString("user")
if userID == "" {
continue
}
ref.Set("user", userID)
if err := app.Save(ref); err != nil {
return err
}
}
}
return nil
}, nil)
}
func migrateLegacyIntegrationsToPluginInstances(app core.App) error {
if _, err := app.FindCollectionByNameOrId("integrations"); err != nil {
return nil
}
records, err := app.FindAllRecords("integrations")
if err != nil {
return err
}
for _, record := range records {
userID := record.GetString("user")
if userID == "" {
continue
}
if raw := legacyJSONObject(record.GetString("strava")); legacyHasValue(raw["clientId"]) {
auth := legacyPick(raw, "clientId", "clientSecret", "accessToken", "refreshToken", "expiresAt", "tokenType", "scope")
legacyNormalizeStravaAuth(auth)
hostConfig := legacyPick(raw, "privacy", "merge")
hostConfig["planned"] = legacyBool(raw["routes"])
hostConfig["completed"] = legacyBool(raw["activities"])
config := legacyNamespacedPluginConfig(
legacyPick(raw, "after"),
hostConfig,
)
if err := saveLegacyMappedPluginInstance(app, userID, "strava", auth, config, raw); err != nil {
return err
}
}
if raw := legacyJSONObject(record.GetString("komoot")); legacyHasValue(raw["email"]) {
auth := legacyPick(raw, "email", "password")
config := legacyNamespacedPluginConfig(
legacyPick(raw, "after"),
legacyPick(raw, "planned", "completed", "privacy", "merge"),
)
if err := saveLegacyMappedPluginInstance(app, userID, "komoot", auth, config, raw); err != nil {
return err
}
}
if raw := legacyJSONObject(record.GetString("hammerhead")); legacyHasValue(raw["email"]) {
auth := legacyPick(raw, "email", "password")
config := legacyNamespacedPluginConfig(
legacyPick(raw, "after"),
legacyPick(raw, "planned", "completed", "privacy", "merge"),
)
if err := saveLegacyMappedPluginInstance(app, userID, "hammerhead", auth, config, raw); err != nil {
return err
}
}
}
return nil
}
func legacyNamespacedPluginConfig(pluginConfig map[string]any, hostConfig map[string]any) map[string]any {
return map[string]any{
"plugin": nilMap(pluginConfig),
"host": nilMap(hostConfig),
}
}
func saveLegacyMappedPluginInstance(app core.App, userID string, pluginID string, auth map[string]any, config map[string]any, raw map[string]any) error {
enabled := legacyBool(raw["active"]) && legacyPluginAuthComplete(pluginID, auth)
return saveLegacyPluginInstance(app, legacyPluginInstance{
UserID: userID,
PluginID: pluginID,
Enabled: enabled,
Auth: auth,
Config: config,
State: map[string]any{},
Status: legacyPluginInstanceStatus(pluginID, auth, enabled, ""),
LastError: map[string]any{},
})
}
type legacyPluginInstance struct {
UserID string
PluginID string
Enabled bool
Auth map[string]any
Config map[string]any
State map[string]any
Status string
LastError map[string]any
LastSyncAt string
RetryNotBefore string
}
func saveLegacyPluginInstance(app core.App, instance legacyPluginInstance) error {
if instance.UserID == "" || instance.PluginID == "" {
return nil
}
existing, _ := app.FindFirstRecordByFilter(
"plugin_instances",
"user={:user} && plugin_id={:plugin_id}",
dbx.Params{"user": instance.UserID, "plugin_id": instance.PluginID},
)
if existing != nil {
return nil
}
authJSON, err := json.Marshal(nilMap(instance.Auth))
if err != nil {
return err
}
configJSON, err := json.Marshal(nilMap(instance.Config))
if err != nil {
return err
}
stateJSON, err := json.Marshal(nilMap(instance.State))
if err != nil {
return err
}
lastErrorJSON, err := json.Marshal(nilMap(instance.LastError))
if err != nil {
return err
}
status := instance.Status
if status == "" {
status = legacyPluginInstanceStatus(instance.PluginID, instance.Auth, instance.Enabled, "")
}
now := types.NowDateTime().String()
_, err = app.DB().Insert("plugin_instances", dbx.Params{
"id": security.RandomStringWithAlphabet(15, "abcdefghijklmnopqrstuvwxyz0123456789"),
"user": instance.UserID,
"plugin_id": instance.PluginID,
"enabled": instance.Enabled,
"auth": string(authJSON),
"config": string(configJSON),
"state": string(stateJSON),
"status": status,
"last_error": string(lastErrorJSON),
"last_sync_at": instance.LastSyncAt,
"retry_not_before": instance.RetryNotBefore,
"created": now,
"updated": now,
}).Execute()
return err
}
func legacyPluginInstanceStatus(pluginID string, auth map[string]any, enabled bool, previous string) string {
if !legacyPluginAuthComplete(pluginID, auth) {
return "needs_auth"
}
if !enabled {
return "disabled"
}
switch previous {
case "configured", "needs_reauth", "syncing", "rate_limited", "unavailable", "unsupported_protocol", "error":
return previous
default:
return "configured"
}
}
func legacyPluginAuthComplete(pluginID string, auth map[string]any) bool {
switch pluginID {
case "strava":
return legacyHasValue(auth["clientId"]) && legacyHasValue(auth["clientSecret"]) && legacyHasValue(auth["refreshToken"])
case "komoot", "hammerhead":
return legacyHasValue(auth["email"]) && legacyHasValue(auth["password"])
default:
return false
}
}
func legacyNormalizeStravaAuth(auth map[string]any) {
legacyStringAuthFields(auth, "clientId", "clientSecret", "accessToken", "refreshToken", "tokenType", "scope")
switch value := auth["expiresAt"].(type) {
case float64:
if value > 0 {
auth["expiresAt"] = time.Unix(int64(value), 0).UTC().Format(time.RFC3339)
}
case int64:
if value > 0 {
auth["expiresAt"] = time.Unix(value, 0).UTC().Format(time.RFC3339)
}
case int:
if value > 0 {
auth["expiresAt"] = time.Unix(int64(value), 0).UTC().Format(time.RFC3339)
}
}
}
func legacyStringAuthFields(auth map[string]any, keys ...string) {
for _, key := range keys {
switch value := auth[key].(type) {
case string:
// already normalized
case float64:
auth[key] = strconv.FormatFloat(value, 'f', -1, 64)
case int64:
auth[key] = strconv.FormatInt(value, 10)
case int:
auth[key] = strconv.Itoa(value)
case nil:
// leave absent/null values untouched so completeness checks still fail
default:
auth[key] = fmt.Sprint(value)
}
}
}
func legacyJSONObject(raw string) map[string]any {
if raw == "" {
return map[string]any{}
}
var data map[string]any
if err := json.Unmarshal([]byte(raw), &data); err != nil || data == nil {
return map[string]any{}
}
return data
}
func legacyPick(src map[string]any, keys ...string) map[string]any {
out := map[string]any{}
for _, key := range keys {
if value, ok := src[key]; ok && value != nil {
out[key] = value
}
}
return out
}
func legacyHasValue(value any) bool {
switch v := value.(type) {
case nil:
return false
case string:
return strings.TrimSpace(v) != ""
default:
return true
}
}
func legacyBool(value any) bool {
b, _ := value.(bool)
return b
}
func nilMap(value map[string]any) map[string]any {
if value == nil {
return map[string]any{}
}
return value
}

View File

@@ -0,0 +1,198 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
return createInstalledPluginsCollection(app)
}, func(app core.App) error {
if collection, err := app.FindCollectionByNameOrId("installed_plugins"); err == nil {
if err := app.Delete(collection); err != nil {
return err
}
}
return nil
})
}
func createInstalledPluginsCollection(app core.App) error {
if _, err := app.FindCollectionByNameOrId("installed_plugins"); err == nil {
return nil
}
jsonData := `{
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "textplginsid01",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"hidden": false,
"id": "textplginpid1",
"max": 128,
"min": 1,
"name": "plugin_id",
"pattern": "^[a-z0-9][a-z0-9_-]*$",
"presentable": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "textplginname",
"max": 256,
"min": 1,
"name": "name",
"pattern": "",
"presentable": true,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "selectplgtype",
"maxSelect": 1,
"name": "type",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": ["trails"]
},
{
"hidden": false,
"id": "textplginvers",
"max": 64,
"min": 1,
"name": "version",
"pattern": "",
"presentable": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "textplginrunt",
"max": 32,
"min": 1,
"name": "runtime",
"pattern": "",
"presentable": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "textplginpath",
"max": 0,
"min": 0,
"name": "path",
"pattern": "",
"presentable": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "jsonplginman",
"maxSize": 2000000,
"name": "manifest",
"presentable": false,
"required": true,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "jsonplgincfg",
"maxSize": 2000000,
"name": "config",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "selectplginst",
"maxSelect": 1,
"name": "status",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": ["available", "disabled", "error"]
},
{
"hidden": false,
"id": "textplginerr",
"max": 0,
"min": 0,
"name": "error",
"pattern": "",
"presentable": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autoplgcreate",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autoplgupdate",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_430002000",
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_installed_plugins_plugin_id` + "`" + ` ON ` + "`" + `installed_plugins` + "`" + ` (` + "`" + `plugin_id` + "`" + `)"
],
"listRule": null,
"name": "installed_plugins",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
}`
collection := &core.Collection{}
if err := json.Unmarshal([]byte(jsonData), collection); err != nil {
return err
}
return app.Save(collection)
}

View File

@@ -0,0 +1,46 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/tools/filesystem"
)
func init() {
m.Register(func(app core.App) error {
categories, err := app.FindAllRecords("categories")
if err != nil {
return err
}
if len(categories) == 0 {
return nil
}
existing, _ := app.FindFirstRecordByData("categories", "name", "Other")
if existing != nil {
return nil
}
collection, err := app.FindCollectionByNameOrId("categories")
if err != nil {
return err
}
record := core.NewRecord(collection)
record.Set("name", "Other")
record.Set("settings", map[string]any{
"wp_merge_enabled": true,
"wp_merge_radius": 50,
})
if file, err := filesystem.NewFileFromPath("migrations/initial_data/other.jpg"); err == nil {
record.Set("img", file)
}
return app.Save(record)
}, func(app core.App) error {
record, _ := app.FindFirstRecordByData("categories", "name", "Other")
if record == nil {
return nil
}
return app.Delete(record)
})
}

View File

@@ -0,0 +1,262 @@
package migrations
import (
"strings"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
const providerBackupColumn1780000006 = "provider_backup_1780000006"
const userPluginIndex1780000006 = "CREATE INDEX `idx_trail_external_reference_user_plugin_id` ON `trail_external_reference` (`user`, `plugin_id`)"
func init() {
m.Register(func(app core.App) error {
if err := backupProviderColumn1780000006(app); err != nil {
return err
}
collection, err := app.FindCollectionByNameOrId("trail_external_reference")
if err != nil {
return err
}
// Drop+re-add (with a new id) blanks the provider column, so the
// provider-scoped unique indexes must not be rebuilt until the values
// have been restored, otherwise a cross-provider external_id clash would
// fail index creation and abort the migration.
removedIndexes := stripProviderIndexes1780000006(collection)
collection.Fields.RemoveByName("provider")
if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{
"autogeneratePattern": "",
"hidden": false,
"id": "text420001002",
"max": 128,
"min": 1,
"name": "provider",
"pattern": "^[a-z0-9][a-z0-9_-]*$",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if collection.Fields.GetByName("plugin_id") == nil {
if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{
"autogeneratePattern": "",
"hidden": false,
"id": "textpluginref",
"max": 64,
"min": 0,
"name": "plugin_id",
"pattern": "^[a-z0-9][a-z0-9_-]*$",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
}
if collection.Fields.GetByName("provider_category") == nil {
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
"autogeneratePattern": "",
"hidden": false,
"id": "txtrmtecat01",
"max": 255,
"min": 0,
"name": "provider_category",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
}
if collection.Fields.GetByName("provider_category_checked_at") == nil {
if err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{
"hidden": false,
"id": "datermtecat1",
"max": "",
"min": "",
"name": "provider_category_checked_at",
"presentable": false,
"required": false,
"system": false,
"type": "date"
}`)); err != nil {
return err
}
}
if err := app.Save(collection); err != nil {
return err
}
if err := restoreProviderColumn1780000006(app); err != nil {
return err
}
collection.Indexes = append(collection.Indexes, removedIndexes...)
if !hasIndex1780000006(collection, userPluginIndex1780000006) {
collection.Indexes = append(collection.Indexes, userPluginIndex1780000006)
}
if err := app.Save(collection); err != nil {
return err
}
refs, err := app.FindAllRecords("trail_external_reference")
if err != nil {
return err
}
for _, ref := range refs {
if ref.GetString("plugin_id") != "" {
continue
}
ref.Set("plugin_id", ref.GetString("provider"))
if err := app.Save(ref); err != nil {
return err
}
}
return nil
}, func(app core.App) error {
if err := backupProviderColumn1780000006(app); err != nil {
return err
}
collection, err := app.FindCollectionByNameOrId("trail_external_reference")
if err != nil {
return err
}
removedIndexes := stripProviderIndexes1780000006(collection)
removeIndex1780000006(collection, userPluginIndex1780000006)
collection.Fields.RemoveByName("provider_category_checked_at")
collection.Fields.RemoveByName("provider_category")
collection.Fields.RemoveByName("plugin_id")
collection.Fields.RemoveByName("provider")
if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{
"hidden": false,
"id": "select420001002",
"maxSelect": 1,
"name": "provider",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": [
"strava",
"komoot",
"hammerhead"
]
}`)); err != nil {
return err
}
if err := app.Save(collection); err != nil {
return err
}
if err := restoreProviderColumn1780000006(app); err != nil {
return err
}
collection.Indexes = append(collection.Indexes, removedIndexes...)
return app.Save(collection)
})
}
// stripProviderIndexes1780000006 removes the provider-scoped indexes from the
// collection metadata and returns them so they can be re-added once the
// provider values have been restored. PocketBase rebuilds indexes from the
// collection metadata on every save; leaving the provider indexes in place
// while the column is transiently empty risks a unique-constraint failure.
func stripProviderIndexes1780000006(collection *core.Collection) []string {
kept := make([]string, 0, len(collection.Indexes))
removed := make([]string, 0)
for _, idx := range collection.Indexes {
if strings.Contains(idx, "`provider`") {
removed = append(removed, idx)
continue
}
kept = append(kept, idx)
}
collection.Indexes = kept
return removed
}
func hasIndex1780000006(collection *core.Collection, index string) bool {
for _, existing := range collection.Indexes {
if existing == index {
return true
}
}
return false
}
func removeIndex1780000006(collection *core.Collection, index string) {
indexes := collection.Indexes[:0]
for _, existing := range collection.Indexes {
if existing == index {
continue
}
indexes = append(indexes, existing)
}
collection.Indexes = indexes
}
func backupProviderColumn1780000006(app core.App) error {
exists, err := columnExists1780000006(app, providerBackupColumn1780000006)
if err != nil {
return err
}
if exists {
return nil
}
if _, err := app.DB().
NewQuery("ALTER TABLE trail_external_reference ADD COLUMN " + providerBackupColumn1780000006 + " TEXT DEFAULT '' NOT NULL").
Execute(); err != nil {
return err
}
_, err = app.DB().
NewQuery("UPDATE trail_external_reference SET " + providerBackupColumn1780000006 + " = provider").
Execute()
return err
}
func restoreProviderColumn1780000006(app core.App) error {
if _, err := app.DB().
NewQuery("UPDATE trail_external_reference SET provider = " + providerBackupColumn1780000006).
Execute(); err != nil {
return err
}
_, err := app.DB().DropColumn("trail_external_reference", providerBackupColumn1780000006).Execute()
return err
}
func columnExists1780000006(app core.App, column string) (bool, error) {
columns, err := app.TableColumns("trail_external_reference")
if err != nil {
return false, err
}
for _, existing := range columns {
if existing == column {
return true, nil
}
}
return false, nil
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB