Skip to content

Commit

Permalink
Refactor and add support for admin management frontend.
Browse files Browse the repository at this point in the history
- Move Lang{} definitions to `search` package.
- Add several CRUD API endpoints (SQL queries, search.* methods)
  for managing entries and relations.
- Add `/api/config`.
- Add HTML template handlers for serving `/admin/*` pages.
- Refactor `Tokenizer` interface functions.
- Remove redundant `types[]` param from `Entry`.
- Refactor `Entry.Relations` into a new `Relations` type.
- Add `search.TokensToTSVector()` for tokenizer plugins to share.
  • Loading branch information
knadh committed Nov 28, 2021
1 parent dec4642 commit 5775371
Show file tree
Hide file tree
Showing 11 changed files with 793 additions and 145 deletions.
8 changes: 2 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,17 @@ build:
go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}'" cmd/${BIN}/*.go

.PHONY: run
run: build
run: build build-tokenizers
./${BIN}

.PHONY: run-admin-frontend
run-admin-frontend:
cd admin && yarn serve

.PHONY: build-tokenizers
build-tokenizers:
go build -ldflags="-s -w" -buildmode=plugin -o kannada.tk tokenizers/kannada/kannada.go
go build -ldflags="-s -w" -buildmode=plugin -o malayalam.tk tokenizers/malayalam/malayalam.go

# Compile bin and bundle static assets.
.PHONY: dist
dist: build
dist: build build-tokenizers
stuffbin -a stuff -in ${BIN} -out ${BIN} ${STATIC}

# pack-releases runn stuffbin packing on the given binary. This is used
Expand Down
221 changes: 220 additions & 1 deletion cmd/dictmaker/admin.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,232 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"

"github.com/go-chi/chi"
"github.com/knadh/dictmaker/internal/search"
)

// handleGetConfig returns the language configuration.
func handleGetConfig(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
)

sendResponse(app.lang, http.StatusOK, w)
out := struct {
RootURL string `json:"root_url"`
Languages search.LangMap `json:"languages"`
Version string `json:"version"`
}{app.constants.RootURL, app.search.Langs, buildString}

sendResponse(out, http.StatusOK, w)
}

// handleGetStats returns DB statistics.
func handleGetStats(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
)

out, err := app.search.GetStats()
if err != nil {
sendErrorResponse(err.Error(), http.StatusInternalServerError, nil, w)
return
}

sendResponse(out, http.StatusOK, w)
}

func handleAdminEntryPage(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
)

sendTpl(http.StatusOK, "entry", app.adminTpl, nil, w)
}

// handleInsertEntry inserts a new dictionary entry.
func handleInsertEntry(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
)

var e search.Entry
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
sendErrorResponse(fmt.Sprintf("error parsing request: %v", err), http.StatusBadRequest, nil, w)
return
}

if err := validateEntry(e); err != nil {
sendErrorResponse(err.Error(), http.StatusBadRequest, nil, w)
return
}

guid, err := app.search.InsertEntry(e)
if err != nil {
sendErrorResponse(fmt.Sprintf("error inserting entry: %v", err), http.StatusInternalServerError, nil, w)
return
}

// Proxy to the get request to respond with the newly inserted entry.
ctx := chi.RouteContext(r.Context())
ctx.URLParams.Keys = append(ctx.URLParams.Keys, "guid")
ctx.URLParams.Values = append(ctx.URLParams.Values, guid)

handleGetEntry(w, r)
}

// handleUpdateEntry updates a dictionary entry.
func handleUpdateEntry(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
guid = chi.URLParam(r, "guid")
)

var e search.Entry
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
sendErrorResponse(fmt.Sprintf("error parsing request: %v", err), http.StatusBadRequest, nil, w)
return
}

if err := app.search.UpdateEntry(guid, e); err != nil {
sendErrorResponse(fmt.Sprintf("error updating entry: %v", err), http.StatusInternalServerError, nil, w)
return
}

sendResponse(app.search.Langs, http.StatusOK, w)
}

// handleDeleteEntry deletes a dictionary entry.
func handleDeleteEntry(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
guid = chi.URLParam(r, "guid")
)

if err := app.search.DeleteEntry(guid); err != nil {
sendErrorResponse(fmt.Sprintf("error deleting entry: %v", err), http.StatusInternalServerError, nil, w)
return
}

sendResponse(true, http.StatusOK, w)
}

// handleAddRelation updates a relation's properties.
func handleAddRelation(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
fromGuid = chi.URLParam(r, "fromGuid")
toGuid = chi.URLParam(r, "toGuid")
)

var rel search.Relation
if err := json.NewDecoder(r.Body).Decode(&rel); err != nil {
sendErrorResponse(fmt.Sprintf("error parsing request: %v", err), http.StatusBadRequest, nil, w)
return
}

if err := app.search.InsertRelation(fromGuid, toGuid, rel); err != nil {
sendErrorResponse(fmt.Sprintf("error updating relation: %v", err), http.StatusInternalServerError, nil, w)
return
}

sendResponse(app.search.Langs, http.StatusOK, w)
}

// handleUpdateRelation updates a relation's properties.
func handleUpdateRelation(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
relID, _ = strconv.Atoi(chi.URLParam(r, "relID"))
)

var rel search.Relation
if err := json.NewDecoder(r.Body).Decode(&rel); err != nil {
sendErrorResponse(fmt.Sprintf("error parsing request: %v", err), http.StatusBadRequest, nil, w)
return
}

if err := app.search.UpdateRelation(relID, rel); err != nil {
sendErrorResponse(fmt.Sprintf("error updating relation: %v", err), http.StatusInternalServerError, nil, w)
return
}

sendResponse(app.search.Langs, http.StatusOK, w)
}

// handleReorderRelations reorders the weights of the relation IDs in the given order.
func handleReorderRelations(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
)

// ids := struct {
// IDs []int `json:ids`
// }{}
var ids []int
if err := json.NewDecoder(r.Body).Decode(&ids); err != nil {
sendErrorResponse(fmt.Sprintf("error parsing request: %v", err), http.StatusBadRequest, nil, w)
return
}

if err := app.search.ReorderRelations(ids); err != nil {
sendErrorResponse(fmt.Sprintf("error reordering relations: %v", err), http.StatusInternalServerError, nil, w)
return
}

sendResponse(true, http.StatusOK, w)
}

// handleDeleteRelation deletes a relation between two entres.
func handleDeleteRelation(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
fromGuid = chi.URLParam(r, "fromGuid")
toGuid = chi.URLParam(r, "toGuid")
)

if err := app.search.DeleteRelation(fromGuid, toGuid); err != nil {
sendErrorResponse(fmt.Sprintf("error deleting entry: %v", err), http.StatusInternalServerError, nil, w)
return
}

sendResponse(true, http.StatusOK, w)
}

func adminPage(tpl string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
app = r.Context().Value("app").(*App)
)

title := ""
switch tpl {
case "search":
title = fmt.Sprintf("Search '%s'", r.URL.Query().Get("query"))
}

sendTpl(http.StatusOK, tpl, app.adminTpl, struct {
Title string
}{title}, w)
})
}

func validateEntry(e search.Entry) error {
if strings.TrimSpace(e.Content) == "" {
return errors.New("invalid `content`.")
}
if strings.TrimSpace(e.Initial) == "" {
return errors.New("invalid `initial`.")
}
if strings.TrimSpace(e.Lang) == "" {
return errors.New("invalid `lang`.")
}

return nil
}

0 comments on commit 5775371

Please sign in to comment.