add new functionality and improve backend server structure
- Enhance server functionality and optimize backend code - Add multi-database support (PostgreSQL, MySQL, SQLite)
This commit is contained in:
@@ -0,0 +1,612 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
func executeEdgeFunction(app core.App, fn edgeFunction, record *core.Record) {
|
||||
start := time.Now()
|
||||
vm := goja.New()
|
||||
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
||||
|
||||
var logs []string
|
||||
outputMu := sync.Mutex{}
|
||||
|
||||
// Basic binds
|
||||
vm.Set("$app", app)
|
||||
if record != nil && record.Id != "" {
|
||||
vm.Set("$record", record)
|
||||
} else {
|
||||
// Dummy record for testing
|
||||
vm.Set("$record", map[string]any{
|
||||
"id": "test_id",
|
||||
"get": func(field string) string {
|
||||
return "test_" + field
|
||||
},
|
||||
"collection": func() map[string]any {
|
||||
return map[string]any{
|
||||
"name": fn.TriggerCollection.String,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Simple console.log implementation
|
||||
vm.Set("console", map[string]any{
|
||||
"log": func(args ...any) {
|
||||
outputMu.Lock()
|
||||
logs = append(logs, fmt.Sprint(args...))
|
||||
outputMu.Unlock()
|
||||
app.Logger().Info("Edge Function Log", "name", fn.Name, "message", args)
|
||||
},
|
||||
})
|
||||
|
||||
// Add fetch polyfill
|
||||
vm.Set("fetch", func(url string, options map[string]any) (goja.Value, error) {
|
||||
method := "GET"
|
||||
var body io.Reader
|
||||
headers := make(map[string]string)
|
||||
|
||||
if options != nil {
|
||||
if m, ok := options["method"].(string); ok {
|
||||
method = strings.ToUpper(m)
|
||||
}
|
||||
if h, ok := options["headers"].(map[string]any); ok {
|
||||
for k, v := range h {
|
||||
headers[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
if b, ok := options["body"].(string); ok {
|
||||
body = strings.NewReader(b)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
// Parse JSON if possible
|
||||
var jsonBody any
|
||||
_ = json.Unmarshal(respBody, &jsonBody)
|
||||
|
||||
// Create response object
|
||||
responseObj := map[string]any{
|
||||
"ok": resp.StatusCode >= 200 && resp.StatusCode < 300,
|
||||
"status": resp.StatusCode,
|
||||
"statusText": resp.Status,
|
||||
"json": func() (goja.Value, error) {
|
||||
p, resolve, _ := vm.NewPromise()
|
||||
resolve(vm.ToValue(jsonBody))
|
||||
return vm.ToValue(p), nil
|
||||
},
|
||||
"text": func() (goja.Value, error) {
|
||||
p, resolve, _ := vm.NewPromise()
|
||||
resolve(vm.ToValue(string(respBody)))
|
||||
return vm.ToValue(p), nil
|
||||
},
|
||||
}
|
||||
|
||||
// Return a resolved promise
|
||||
promise, resolve, _ := vm.NewPromise()
|
||||
resolve(vm.ToValue(responseObj))
|
||||
return vm.ToValue(promise), nil
|
||||
})
|
||||
|
||||
// Execute the body
|
||||
_, err := vm.RunString(fn.Body.String)
|
||||
duration := time.Since(start).Milliseconds()
|
||||
output := strings.Join(logs, "\n")
|
||||
|
||||
if err != nil {
|
||||
app.Logger().Error("Edge Function Execution Failed", "name", fn.Name, "error", err.Error())
|
||||
logEdgeFunctionExecution(app, fn.Id, "error", duration, output, err.Error())
|
||||
} else {
|
||||
app.Logger().Info("Edge Function Executed Successfully", "name", fn.Name)
|
||||
logEdgeFunctionExecution(app, fn.Id, "success", duration, output, "")
|
||||
}
|
||||
}
|
||||
|
||||
type edgeFunction struct {
|
||||
Id string `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Body sql.NullString `db:"body" json:"body"`
|
||||
TriggerType sql.NullString `db:"trigger_type" json:"trigger_type"`
|
||||
TriggerConfig sql.NullString `db:"trigger_config" json:"trigger_config"`
|
||||
Active bool `db:"active" json:"active"`
|
||||
TriggerCollection sql.NullString `db:"trigger_collection" json:"trigger_collection"`
|
||||
TriggerEvent sql.NullString `db:"trigger_event" json:"trigger_event"`
|
||||
Created sql.NullTime `db:"created" json:"created"`
|
||||
Updated sql.NullTime `db:"updated" json:"updated"`
|
||||
}
|
||||
|
||||
func logEdgeFunctionExecution(app core.App, fnId string, status string, duration int64, output string, errStr string) {
|
||||
id := security.PseudorandomStringWithAlphabet(15, "abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
_, err := app.NonconcurrentDB().Insert("_edge_function_logs", dbx.Params{
|
||||
"id": id,
|
||||
"function_id": fnId,
|
||||
"status": status,
|
||||
"duration": duration,
|
||||
"output": output,
|
||||
"error": errStr,
|
||||
}).Execute()
|
||||
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to save edge function log", "error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func reloadCronJobs(app core.App) {
|
||||
if app.Cron() == nil {
|
||||
app.Logger().Warn("Cron is not initialized, skipping reload")
|
||||
return
|
||||
}
|
||||
app.Logger().Info("Reloading edge function cron jobs")
|
||||
// Remove all existing edge function cron jobs
|
||||
for _, job := range app.Cron().Jobs() {
|
||||
if strings.HasPrefix(job.Id(), "edge_") {
|
||||
app.Logger().Info("Removing cron job", "id", job.Id())
|
||||
app.Cron().Remove(job.Id())
|
||||
}
|
||||
}
|
||||
|
||||
var functions []edgeFunction
|
||||
err := app.DB().
|
||||
Select("*").
|
||||
From("_edge_functions").
|
||||
Where(dbx.HashExp{
|
||||
"active": true,
|
||||
"trigger_type": "cron",
|
||||
}).
|
||||
All(&functions)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, fn := range functions {
|
||||
cronExpr := fn.TriggerConfig.String
|
||||
if cronExpr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fnCopy := fn // capture for closure
|
||||
jobName := "edge_" + fn.Id
|
||||
app.Cron().Add(jobName, cronExpr, func() {
|
||||
executeEdgeFunction(app, fnCopy, nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// bindEdgeFunctionsApi registers the Edge Functions API routes.
|
||||
func bindEdgeFunctionsApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
|
||||
subGroup := rg.Group("/functions").Bind(RequireSuperuserAuth())
|
||||
|
||||
// Ensure the table exists on startup
|
||||
ensureEdgeFunctionsTable(app)
|
||||
|
||||
// Load cron jobs and register webhook on startup
|
||||
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
|
||||
// Register HTTP Trigger Route (Public)
|
||||
// We register it here to avoid conflicts with the static file server
|
||||
e.Router.POST("/edge-hook/{name}", func(e *core.RequestEvent) error {
|
||||
name := e.Request.PathValue("name")
|
||||
|
||||
var fn edgeFunction
|
||||
err := e.App.DB().
|
||||
Select("*").
|
||||
From("_edge_functions").
|
||||
Where(dbx.HashExp{
|
||||
"active": true,
|
||||
"trigger_type": "http",
|
||||
"name": name,
|
||||
}).
|
||||
One(&fn)
|
||||
|
||||
if err != nil {
|
||||
return e.NotFoundError("Function not found or inactive.", nil)
|
||||
}
|
||||
|
||||
// Execute function
|
||||
go executeEdgeFunction(e.App, fn, nil)
|
||||
|
||||
return e.JSON(http.StatusOK, map[string]any{
|
||||
"message": "Function triggered successfully",
|
||||
})
|
||||
})
|
||||
|
||||
reloadCronJobs(e.App)
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// Register hooks to trigger functions
|
||||
app.OnRecordAfterCreateSuccess().BindFunc(func(e *core.RecordEvent) error {
|
||||
collectionName := e.Record.Collection().Name
|
||||
|
||||
var functions []edgeFunction
|
||||
err := e.App.DB().
|
||||
Select("*").
|
||||
From("_edge_functions").
|
||||
Where(dbx.HashExp{
|
||||
"active": true,
|
||||
"trigger_type": "record",
|
||||
"trigger_collection": collectionName,
|
||||
"trigger_event": "create",
|
||||
}).
|
||||
All(&functions)
|
||||
|
||||
if err != nil {
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
for _, fn := range functions {
|
||||
go executeEdgeFunction(e.App, fn, e.Record)
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
app.OnRecordAfterUpdateSuccess().BindFunc(func(e *core.RecordEvent) error {
|
||||
var functions []edgeFunction
|
||||
err := e.App.DB().
|
||||
Select("*").
|
||||
From("_edge_functions").
|
||||
Where(dbx.HashExp{
|
||||
"active": true,
|
||||
"trigger_type": "record",
|
||||
"trigger_collection": e.Record.Collection().Name,
|
||||
"trigger_event": "update",
|
||||
}).
|
||||
All(&functions)
|
||||
|
||||
if err == nil {
|
||||
for _, fn := range functions {
|
||||
go executeEdgeFunction(e.App, fn, e.Record)
|
||||
}
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
app.OnRecordAfterDeleteSuccess().BindFunc(func(e *core.RecordEvent) error {
|
||||
var functions []edgeFunction
|
||||
err := e.App.DB().
|
||||
Select("*").
|
||||
From("_edge_functions").
|
||||
Where(dbx.HashExp{
|
||||
"active": true,
|
||||
"trigger_type": "record",
|
||||
"trigger_collection": e.Record.Collection().Name,
|
||||
"trigger_event": "delete",
|
||||
}).
|
||||
All(&functions)
|
||||
|
||||
if err == nil {
|
||||
for _, fn := range functions {
|
||||
go executeEdgeFunction(e.App, fn, e.Record)
|
||||
}
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// Logs API
|
||||
subGroup.GET("/{id}/logs", func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
|
||||
var logs []map[string]any
|
||||
err := e.App.DB().
|
||||
Select("*").
|
||||
From("_edge_function_logs").
|
||||
Where(dbx.HashExp{"function_id": id}).
|
||||
OrderBy("created DESC").
|
||||
Limit(50).
|
||||
All(&logs)
|
||||
|
||||
if err != nil {
|
||||
return e.BadRequestError("Failed to load logs", err)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, logs)
|
||||
})
|
||||
|
||||
subGroup.GET("", func(e *core.RequestEvent) error {
|
||||
var functions []edgeFunction
|
||||
e.App.Logger().Info("Executing GET /api/functions")
|
||||
|
||||
// Use app.DB() which is the standard way to access the data database
|
||||
err := e.App.DB().
|
||||
Select("*").
|
||||
From("_edge_functions").
|
||||
OrderBy("created DESC").
|
||||
All(&functions)
|
||||
|
||||
if err != nil {
|
||||
e.App.Logger().Error("Failed to load edge functions", "error", err.Error())
|
||||
return e.BadRequestError("Failed to load edge functions: "+err.Error(), nil)
|
||||
}
|
||||
|
||||
e.App.Logger().Info("Loaded edge functions from DB", "count", len(functions))
|
||||
for _, f := range functions {
|
||||
e.App.Logger().Info("Function in list", "id", f.Id, "name", f.Name)
|
||||
}
|
||||
|
||||
// Convert to response format with string dates
|
||||
result := make([]map[string]any, len(functions))
|
||||
for i, f := range functions {
|
||||
created := ""
|
||||
if f.Created.Valid {
|
||||
created = f.Created.Time.Format("2006-01-02 15:04:05.000Z")
|
||||
}
|
||||
updated := ""
|
||||
if f.Updated.Valid {
|
||||
updated = f.Updated.Time.Format("2006-01-02 15:04:05.000Z")
|
||||
}
|
||||
|
||||
// Debug log each function ID and Name
|
||||
e.App.Logger().Debug("Function row", "id", f.Id, "name", f.Name)
|
||||
|
||||
result[i] = map[string]any{
|
||||
"id": f.Id,
|
||||
"name": f.Name,
|
||||
"body": f.Body.String,
|
||||
"trigger_type": f.TriggerType.String,
|
||||
"trigger_config": f.TriggerConfig.String,
|
||||
"active": f.Active,
|
||||
"trigger_collection": f.TriggerCollection.String,
|
||||
"trigger_event": f.TriggerEvent.String,
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
}
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, result)
|
||||
})
|
||||
|
||||
subGroup.POST("", func(e *core.RequestEvent) error {
|
||||
data := struct {
|
||||
Name string `json:"name" form:"name"`
|
||||
Body string `json:"body" form:"body"`
|
||||
TriggerType string `json:"trigger_type" form:"trigger_type"`
|
||||
TriggerConfig string `json:"trigger_config" form:"trigger_config"`
|
||||
Active bool `json:"active" form:"active"`
|
||||
TriggerCollection string `json:"trigger_collection" form:"trigger_collection"`
|
||||
TriggerEvent string `json:"trigger_event" form:"trigger_event"`
|
||||
}{}
|
||||
|
||||
if err := e.BindBody(&data); err != nil {
|
||||
return e.BadRequestError("Failed to parse request body.", err)
|
||||
}
|
||||
|
||||
if data.Name == "" {
|
||||
return e.BadRequestError("Function name is required.", nil)
|
||||
}
|
||||
|
||||
e.App.Logger().Info("Creating function",
|
||||
"name", data.Name,
|
||||
"trigger_col", data.TriggerCollection,
|
||||
"trigger_event", data.TriggerEvent)
|
||||
|
||||
id := security.PseudorandomStringWithAlphabet(15, "abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
|
||||
// Insert the function
|
||||
_, err := e.App.NonconcurrentDB().Insert("_edge_functions", dbx.Params{
|
||||
"id": id,
|
||||
"name": data.Name,
|
||||
"body": data.Body,
|
||||
"trigger_type": data.TriggerType,
|
||||
"trigger_config": data.TriggerConfig,
|
||||
"active": data.Active,
|
||||
"trigger_collection": data.TriggerCollection,
|
||||
"trigger_event": data.TriggerEvent,
|
||||
}).Execute()
|
||||
|
||||
if err != nil {
|
||||
return e.BadRequestError("Failed to create function: "+err.Error(), nil)
|
||||
}
|
||||
|
||||
if data.TriggerType == "cron" {
|
||||
reloadCronJobs(e.App)
|
||||
}
|
||||
|
||||
// Return the created object
|
||||
return e.JSON(http.StatusOK, map[string]any{
|
||||
"id": id,
|
||||
"name": data.Name,
|
||||
"body": data.Body,
|
||||
"trigger_type": data.TriggerType,
|
||||
"trigger_config": data.TriggerConfig,
|
||||
"active": data.Active,
|
||||
"trigger_collection": data.TriggerCollection,
|
||||
"trigger_event": data.TriggerEvent,
|
||||
})
|
||||
})
|
||||
|
||||
subGroup.GET("/{id}", func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
|
||||
var f edgeFunction
|
||||
err := e.App.ConcurrentDB().
|
||||
Select("*").
|
||||
From("_edge_functions").
|
||||
Where(dbx.HashExp{"id": id}).
|
||||
One(&f)
|
||||
|
||||
if err != nil {
|
||||
return e.NotFoundError("Function not found.", nil)
|
||||
}
|
||||
|
||||
created := ""
|
||||
if f.Created.Valid {
|
||||
created = f.Created.Time.Format("2006-01-02 15:04:05.000Z")
|
||||
}
|
||||
updated := ""
|
||||
if f.Updated.Valid {
|
||||
updated = f.Updated.Time.Format("2006-01-02 15:04:05.000Z")
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, map[string]any{
|
||||
"id": f.Id,
|
||||
"name": f.Name,
|
||||
"body": f.Body.String,
|
||||
"trigger_collection": f.TriggerCollection.String,
|
||||
"trigger_event": f.TriggerEvent.String,
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
})
|
||||
})
|
||||
|
||||
subGroup.POST("/{id}/test", func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
|
||||
var f edgeFunction
|
||||
err := e.App.ConcurrentDB().
|
||||
Select("*").
|
||||
From("_edge_functions").
|
||||
Where(dbx.HashExp{"id": id}).
|
||||
One(&f)
|
||||
|
||||
if err != nil {
|
||||
return e.NotFoundError("Function not found.", nil)
|
||||
}
|
||||
|
||||
// Execute with a dummy record
|
||||
go executeEdgeFunction(e.App, f, &core.Record{})
|
||||
|
||||
return e.JSON(http.StatusOK, map[string]any{
|
||||
"message": "Test execution started",
|
||||
})
|
||||
})
|
||||
|
||||
subGroup.PATCH("/{id}", func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
e.App.Logger().Info("PATCH function request", "id", id)
|
||||
|
||||
data := struct {
|
||||
Name string `json:"name" form:"name"`
|
||||
Body string `json:"body" form:"body"`
|
||||
TriggerType string `json:"trigger_type" form:"trigger_type"`
|
||||
TriggerConfig string `json:"trigger_config" form:"trigger_config"`
|
||||
Active bool `json:"active" form:"active"`
|
||||
TriggerCollection string `json:"trigger_collection" form:"trigger_collection"`
|
||||
TriggerEvent string `json:"trigger_event" form:"trigger_event"`
|
||||
}{}
|
||||
|
||||
if err := e.BindBody(&data); err != nil {
|
||||
e.App.Logger().Error("Failed to bind body", "error", err.Error())
|
||||
return e.BadRequestError("Failed to parse request body.", err)
|
||||
}
|
||||
|
||||
e.App.Logger().Info("Updating function",
|
||||
"name", data.Name,
|
||||
"body_len", len(data.Body),
|
||||
"trigger_col", data.TriggerCollection,
|
||||
"trigger_event", data.TriggerEvent)
|
||||
|
||||
_, err := e.App.NonconcurrentDB().Update("_edge_functions", dbx.Params{
|
||||
"name": data.Name,
|
||||
"body": data.Body,
|
||||
"trigger_type": data.TriggerType,
|
||||
"trigger_config": data.TriggerConfig,
|
||||
"active": data.Active,
|
||||
"trigger_collection": data.TriggerCollection,
|
||||
"trigger_event": data.TriggerEvent,
|
||||
}, dbx.HashExp{"id": id}).Execute()
|
||||
|
||||
if err != nil {
|
||||
e.App.Logger().Error("Failed to update function in DB", "error", err.Error())
|
||||
return e.BadRequestError("Failed to update function: "+err.Error(), nil)
|
||||
}
|
||||
|
||||
reloadCronJobs(e.App)
|
||||
|
||||
return e.JSON(http.StatusOK, map[string]any{
|
||||
"id": id,
|
||||
"name": data.Name,
|
||||
"body": data.Body,
|
||||
"trigger_type": data.TriggerType,
|
||||
"trigger_config": data.TriggerConfig,
|
||||
"active": data.Active,
|
||||
"trigger_collection": data.TriggerCollection,
|
||||
"trigger_event": data.TriggerEvent,
|
||||
})
|
||||
})
|
||||
|
||||
subGroup.DELETE("/{id}", func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
|
||||
_, err := e.App.NonconcurrentDB().Delete("_edge_functions", dbx.HashExp{"id": id}).Execute()
|
||||
|
||||
if err != nil {
|
||||
return e.BadRequestError("Failed to delete function: "+err.Error(), nil)
|
||||
}
|
||||
|
||||
reloadCronJobs(e.App)
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
||||
// ensureEdgeFunctionsTable creates the _edge_functions table if it doesn't exist (PostgreSQL compatible)
|
||||
func ensureEdgeFunctionsTable(app core.App) {
|
||||
_, _ = app.NonconcurrentDB().NewQuery(`
|
||||
CREATE TABLE IF NOT EXISTS _edge_functions (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
body TEXT,
|
||||
trigger_type TEXT DEFAULT 'record',
|
||||
trigger_config TEXT,
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
trigger_collection TEXT,
|
||||
trigger_event TEXT,
|
||||
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`).Execute()
|
||||
|
||||
// Add columns if they don't exist (for existing tables)
|
||||
_, _ = app.NonconcurrentDB().NewQuery(`ALTER TABLE _edge_functions ADD COLUMN IF NOT EXISTS trigger_type TEXT DEFAULT 'record'`).Execute()
|
||||
_, _ = app.NonconcurrentDB().NewQuery(`ALTER TABLE _edge_functions ADD COLUMN IF NOT EXISTS trigger_config TEXT`).Execute()
|
||||
_, _ = app.NonconcurrentDB().NewQuery(`ALTER TABLE _edge_functions ADD COLUMN IF NOT EXISTS active BOOLEAN DEFAULT TRUE`).Execute()
|
||||
_, _ = app.NonconcurrentDB().NewQuery(`ALTER TABLE _edge_functions ADD COLUMN IF NOT EXISTS trigger_collection TEXT`).Execute()
|
||||
_, _ = app.NonconcurrentDB().NewQuery(`ALTER TABLE _edge_functions ADD COLUMN IF NOT EXISTS trigger_event TEXT`).Execute()
|
||||
|
||||
// Create logs table
|
||||
_, _ = app.NonconcurrentDB().NewQuery(`
|
||||
CREATE TABLE IF NOT EXISTS _edge_function_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
function_id TEXT NOT NULL,
|
||||
status TEXT,
|
||||
duration INTEGER,
|
||||
output TEXT,
|
||||
error TEXT,
|
||||
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`).Execute()
|
||||
}
|
||||
Reference in New Issue
Block a user