~evanj/cms

ab7c5d17f061cd08da71ea3c54beea9e7ac26a27 — Evan M Jones 1 year, 1 month ago 4aa45cc
Feat(cache): Caching MVP for content only (single).
M cms.go => cms.go +13 -1
@@ 12,6 12,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/c/ping"
	"git.sr.ht/~evanj/cms/internal/c/space"
	"git.sr.ht/~evanj/cms/internal/c/user"
	"git.sr.ht/~evanj/cms/internal/s/cache"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"git.sr.ht/~evanj/cms/pkg/e3"
	"git.sr.ht/~evanj/security"


@@ 83,6 84,16 @@ func init() {
		applogger.Fatal(err)
	}

	cacher, err := cache.New(
		log.New(w, "[cms:cache] ", 0),
		db,
		os.Getenv("MEMCACHE_KEY"),
		os.Getenv("MEMCACHE_SERVER"),
	)
	if err != nil {
		applogger.Fatal(err)
	}

	fs := e3.New(
		os.Getenv("E3_USER"),
		os.Getenv("E3_PASS"),


@@ 93,7 104,8 @@ func init() {
		log: applogger,
		content: content.New(
			log.New(w, "[cms:content] ", 0),
			db,
			cacher,
			// 	db,
			fs,
		),
		contenttype: contenttype.New(

M go.mod => go.mod +1 -0
@@ 4,6 4,7 @@ go 1.12

require (
	git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997
	github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
	github.com/go-playground/assert/v2 v2.0.1
	github.com/go-sql-driver/mysql v1.5.0
)

M go.sum => go.sum +2 -0
@@ 2,6 2,8 @@ git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997 h1:fdAj8fR4mpS/OAve
git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997/go.mod h1:40791KVgThT97CT6mTsF4NUNPeX2BAVlsuH1RiiSrAs=
github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg=
github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=

M internal/m/content/content.go => internal/m/content/content.go +0 -5
@@ 1,16 1,11 @@
package content

import (
	"encoding/json"

	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/value"
)

type Content interface {
	json.Marshaler
	ID() string
	Type() contenttype.ContentType
	Values() []value.Value
	ValueByName(name string) (value.Value, bool)
	MustValueByName(name string) value.Value

M internal/m/contenttype/contenttype.go => internal/m/contenttype/contenttype.go +0 -3
@@ 1,13 1,10 @@
package contenttype

import (
	"encoding/json"

	"git.sr.ht/~evanj/cms/internal/m/valuetype"
)

type ContentType interface {
	json.Marshaler
	ID() string
	Name() string
	Fields() []valuetype.ValueType

M internal/m/space/space.go => internal/m/space/space.go +0 -3
@@ 1,9 1,6 @@
package space

import "encoding/json"

type Space interface {
	json.Marshaler
	ID() string
	Name() string
	Desc() string

M internal/m/user/user.go => internal/m/user/user.go +0 -3
@@ 1,9 1,6 @@
package user

import "encoding/json"

type User interface {
	json.Marshaler
	ID() string
	Name() string
	Token() string

A internal/s/cache/cache.go => internal/s/cache/cache.go +103 -0
@@ 0,0 1,103 @@
package cache

import (
	"encoding/json"
	"fmt"
	"log"

	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"github.com/bradfitz/gomemcache/memcache"
)

type Cache struct {
	*db.DB
	log     *log.Logger
	baseKey string
	mc      *memcache.Client
	db      *db.DB
}

func New(log *log.Logger, db *db.DB, key string, memcacheServer string) (*Cache, error) {
	c := &Cache{
		db,
		log,
		key,
		memcache.New(memcacheServer),
		db,
	}
	return c, c.mc.Ping()
}

func (c *Cache) content(breakCache bool, key string, getter func() (content.Content, error)) (content.Content, error) {
	m, err := c.mc.Get(key)
	if err != nil || breakCache {
		content, err := getter()
		if err != nil {
			return nil, err
		}

		bytes, err := json.Marshal(content)
		if err != nil {
			return nil, err
		}

		if err := c.mc.Set(&memcache.Item{Key: key, Value: bytes}); err != nil {
			return nil, err
		}

		var next db.Content
		if err := json.Unmarshal(bytes, &next); err != nil {
			return nil, err
		}

		c.log.Println("put", key)
		return &next, nil
	}

	var v *db.Content
	if err := json.Unmarshal(m.Value, &v); err != nil {
		return nil, err
	}

	c.log.Println("got", key)
	return v, nil
}

func (c *Cache) ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error) {
	return c.content(
		false,
		fmt.Sprintf("%s::%s::%s::%s", c.baseKey, space.ID(), ct.ID(), contentID),
		func() (content.Content, error) { return c.db.ContentGet(space, ct, contentID) },
	)
}

func (c *Cache) ContentUpdate(space space.Space, ct contenttype.ContentType, item content.Content, params []db.ContentUpdateParam) (content.Content, error) {
	return c.content(
		true,
		fmt.Sprintf("%s::%s::%s::%s", c.baseKey, space.ID(), ct.ID(), item.ID()),
		func() (content.Content, error) { return c.db.ContentUpdate(space, ct, item, params) },
	)
}

func (c *Cache) ContentDelete(space space.Space, ct contenttype.ContentType, item content.Content) error {
	key := fmt.Sprintf("%s::%s::%s::%s", c.baseKey, space.ID(), ct.ID(), item.ID())

	var deleteErr error
	_, _ = c.content(
		true,
		key,
		func() (content.Content, error) {
			deleteErr = c.db.ContentDelete(space, ct, item)
			return nil, deleteErr
		},
	)

	if deleteErr != nil {
		return deleteErr
	}

	return c.mc.Delete(key)
}

A internal/s/cache/cache_test.go => internal/s/cache/cache_test.go +1 -0
@@ 0,0 1,1 @@
package cache_test

M internal/s/db/content.go => internal/s/db/content.go +53 -69
@@ 1,7 1,6 @@
package db

import (
	"encoding/json"
	"fmt"

	"git.sr.ht/~evanj/cms/internal/m/content"


@@ 12,41 11,33 @@ import (
)

type Content struct {
	id            string
	contenttypeID string

	contenttype contenttype.ContentType
	values      []ContentValue
	// In DB.
	ContentID           string
	ContentParentTypeID string
	// We set.
	ContentValues []ContentValue
}

type ContentValue struct {
	id    string
	typ   string // StringSmall
	name  string // Title of a blog post
	value string // "My First Blog Post!"
	FieldID    string
	FieldType  string // StringSmall
	FieldName  string // Title of a blog post
	FieldValue string // "My First Blog Post!"
}

const (
var (
	queryContentNew = `
		INSERT INTO cms_content (CONTENTTYPE_ID) 
		VALUES (?);
	`

	queryContentDelete = `
		-- Delete joined values.
		DELETE FROM cms_value_string_small 
		WHERE ID IN ( SELECT ID FROM cms_value WHERE CONTENT_ID = ? );
		DELETE FROM cms_value_string_big
		WHERE ID IN ( SELECT ID FROM cms_value WHERE CONTENT_ID = ? );
		DELETE FROM cms_value_date
		WHERE ID IN ( SELECT ID FROM cms_value WHERE CONTENT_ID = ? );
		-- Delete attached values.
		DELETE FROM cms_value
		WHERE CONTENT_ID = ?;
		-- Delete content itself.
		DELETE FROM cms_content 
		WHERE ID = ?;
	`
	queryContentDelete = []string{
		"DELETE FROM cms_value_string_small WHERE ID IN ( SELECT ID FROM cms_value WHERE CONTENT_ID = ? );",
		"DELETE FROM cms_value_string_big WHERE ID IN ( SELECT ID FROM cms_value WHERE CONTENT_ID = ? );",
		"DELETE FROM cms_value_date WHERE ID IN ( SELECT ID FROM cms_value WHERE CONTENT_ID = ? );",
		"DELETE FROM cms_value WHERE CONTENT_ID = ?;",
		"DELETE FROM cms_content WHERE ID = ?;",
	}

	queryContentGetByID = `
		SELECT ID, CONTENTTYPE_ID 


@@ 209,7 200,7 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [
	}

	var content Content
	if err := db.QueryRow(queryContentGetByID, contentID).Scan(&content.id, &content.contenttypeID); err != nil {
	if err := db.QueryRow(queryContentGetByID, contentID).Scan(&content.ContentID, &content.ContentParentTypeID); err != nil {
		db.log.Println("db.QueryRow", err)
		return nil, fmt.Errorf("failed to find content created")
	}


@@ 247,18 238,17 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [
		}

		var value ContentValue
		if err := db.QueryRow(queryValueGetTypeByID, valueID).Scan(&value.id, &value.typ, &value.name, &value.value); err != nil {
		if err := db.QueryRow(queryValueGetTypeByID, valueID).Scan(&value.FieldID, &value.FieldType, &value.FieldName, &value.FieldValue); err != nil {
			db.log.Println("db.QueryRow", err)
			return nil, fmt.Errorf("failed to find value created")
		}
		content.values = append(content.values, value)
		content.ContentValues = append(content.ContentValues, value)
	}

	if len(ct.Fields()) != len(content.values) {
	if len(ct.Fields()) != len(content.ContentValues) {
		return nil, fmt.Errorf("failed to create all values")
	}

	content.contenttype = ct
	return &content, nil
}



@@ 283,9 273,21 @@ func (db *DB) ContentUpdate(space space.Space, ct contenttype.ContentType, conte
}

func (db *DB) ContentDelete(space space.Space, ct contenttype.ContentType, content content.Content) error {
	id := content.ID()
	_, err := db.Exec(queryContentDelete, id, id, id, id, id)
	return err
	t, err := db.Begin()
	if err != nil {
		return err
	}

	defer t.Rollback()

	for _, q := range queryContentDelete {
		_, err := db.Exec(q, content.ID())
		if err != nil {
			return err
		}
	}

	return t.Commit()
}

func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentType, page int) ([]content.Content, error) {


@@ 298,11 300,11 @@ func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentTyp

	for rows.Next() {
		var content Content
		if err := rows.Scan(&content.id, &content.contenttypeID); err != nil {
		if err := rows.Scan(&content.ContentID, &content.ContentParentTypeID); err != nil {
			return nil, err
		}

		rows, err := db.Query(queryValueListByContent, content.id, content.id, content.id)
		rows, err := db.Query(queryValueListByContent, content.ContentID, content.ContentID, content.ContentID)
		if err != nil {
			db.log.Println(err)
			return nil, err


@@ 310,10 312,10 @@ func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentTyp

		for rows.Next() {
			var value ContentValue
			if err := rows.Scan(&value.id, &value.typ, &value.name, &value.value); err != nil {
			if err := rows.Scan(&value.FieldID, &value.FieldType, &value.FieldName, &value.FieldValue); err != nil {
				return nil, err
			}
			content.values = append(content.values, value)
			content.ContentValues = append(content.ContentValues, value)
		}

		ret = append(ret, &content)


@@ 324,7 326,7 @@ func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentTyp

func (db *DB) ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error) {
	var content Content
	if err := db.QueryRow(queryContentGetByID, contentID).Scan(&content.id, &content.contenttypeID); err != nil {
	if err := db.QueryRow(queryContentGetByID, contentID).Scan(&content.ContentID, &content.ContentParentTypeID); err != nil {
		db.log.Println("db.QueryRow", err)
		return nil, fmt.Errorf("failed to find space")
	}


@@ 337,13 339,12 @@ func (db *DB) ContentGet(space space.Space, ct contenttype.ContentType, contentI

	for rows.Next() {
		var value ContentValue
		if err := rows.Scan(&value.id, &value.typ, &value.name, &value.value); err != nil {
		if err := rows.Scan(&value.FieldID, &value.FieldType, &value.FieldName, &value.FieldValue); err != nil {
			return nil, fmt.Errorf("failed to scan values(s)")
		}
		content.values = append(content.values, value)
		content.ContentValues = append(content.ContentValues, value)
	}

	content.contenttype = ct
	return &content, nil
}



@@ 371,21 372,17 @@ func (db *DB) valueQuerySetByType(typ valuetype.ValueTypeEnum) (insert, get, upd
}

func (c *Content) ID() string {
	return c.id
}

func (c *Content) Type() contenttype.ContentType {
	return c.contenttype
	return c.ContentID
}

func (c *Content) Values() []value.Value {
	var ret []value.Value
	for _, item := range c.values {
	for _, item := range c.ContentValues {
		ret = append(ret, &ContentValue{
			item.id,
			item.typ,
			item.name,
			item.value,
			item.FieldID,
			item.FieldType,
			item.FieldName,
			item.FieldValue,
		})
	}
	return ret


@@ 408,31 405,18 @@ func (c *Content) MustValueByName(name string) (ret value.Value) {
	return
}

func (c *Content) MarshalJSON() ([]byte, error) {
	fields := make(map[string]string)
	for _, item := range c.Values() {
		fields[item.Name()] = item.Value()
	}

	values := make(map[string]interface{})
	values["id"] = c.ID()
	values["fields"] = fields

	return json.Marshal(values)
}

func (c *ContentValue) ID() string {
	return c.id
	return c.FieldID
}

func (c *ContentValue) Type() string {
	return c.typ
	return c.FieldType
}

func (c *ContentValue) Name() string {
	return c.name
	return c.FieldName
}

func (c *ContentValue) Value() string {
	return c.value
	return c.FieldValue
}

M internal/s/db/contenttype.go => internal/s/db/contenttype.go +25 -37
@@ 1,7 1,6 @@
package db

import (
	"encoding/json"
	"fmt"

	"git.sr.ht/~evanj/cms/internal/m/contenttype"


@@ 10,13 9,15 @@ import (
)

type ContentType struct {
	id     string
	name   string
	fields []ContentTypeField
	ParentID     string
	ParentName   string
	ParentFields []ContentTypeField
}

type ContentTypeField struct {
	id, name, typ string
	FieldID   string
	FieldName string
	FieldType string
}

type ContentTypeNewParam struct {


@@ 73,25 74,25 @@ func (db *DB) ContentTypeNew(space space.Space, name string, params []ContentTyp
	// }

	var ct ContentType
	if err := db.QueryRow(queryFindContentTypeByID, id).Scan(&ct.id, &ct.name); err != nil {
	if err := db.QueryRow(queryFindContentTypeByID, id).Scan(&ct.ParentID, &ct.ParentName); err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find user created")
	}

	rows, err := db.Query(queryFindValueTypes, ct.id)
	rows, err := db.Query(queryFindValueTypes, ct.ParentID)
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find field(s)")
	}
	for rows.Next() {
		var field ContentTypeField
		if err := rows.Scan(&field.id, &field.name, &field.typ); err != nil {
		if err := rows.Scan(&field.FieldID, &field.FieldName, &field.FieldType); err != nil {
			return nil, fmt.Errorf("failed to scan field(s)")
		}
		ct.fields = append(ct.fields, field)
		ct.ParentFields = append(ct.ParentFields, field)
	}

	if len(ct.fields) != len(params) {
	if len(ct.ParentFields) != len(params) {
		return nil, fmt.Errorf("failed to create all fields")
	}



@@ 108,7 109,7 @@ func (db *DB) ContentTypesPerSpace(space space.Space, page int) ([]contenttype.C

	for rows.Next() {
		var ct ContentType
		if err := rows.Scan(&ct.id, &ct.name); err != nil {
		if err := rows.Scan(&ct.ParentID, &ct.ParentName); err != nil {
			return nil, err
		}
		ret = append(ret, &ct)


@@ 119,68 120,55 @@ func (db *DB) ContentTypesPerSpace(space space.Space, page int) ([]contenttype.C

func (db *DB) ContentTypeGet(space space.Space, contenttypeID string) (contenttype.ContentType, error) {
	var ct ContentType
	if err := db.QueryRow(queryFindContentTypeByIDAndSpace, contenttypeID, space.ID()).Scan(&ct.id, &ct.name); err != nil {
	if err := db.QueryRow(queryFindContentTypeByIDAndSpace, contenttypeID, space.ID()).Scan(&ct.ParentID, &ct.ParentName); err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find contenttype for space")
	}

	rows, err := db.Query(queryFindValueTypes, ct.id)
	rows, err := db.Query(queryFindValueTypes, ct.ParentID)
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find field(s)")
	}
	for rows.Next() {
		var field ContentTypeField
		if err := rows.Scan(&field.id, &field.name, &field.typ); err != nil {
		if err := rows.Scan(&field.FieldID, &field.FieldName, &field.FieldType); err != nil {
			return nil, fmt.Errorf("failed to scan field(s)")
		}
		ct.fields = append(ct.fields, field)
		ct.ParentFields = append(ct.ParentFields, field)
	}

	return &ct, nil
}

func (ct *ContentType) ID() string {
	return ct.id
	return ct.ParentID
}

func (ct *ContentType) Name() string {
	return ct.name
	return ct.ParentName
}

func (ct *ContentType) Fields() []valuetype.ValueType {
	var ret []valuetype.ValueType
	for _, item := range ct.fields {
	for _, item := range ct.ParentFields {
		ret = append(ret, &ContentTypeField{
			item.id,
			item.name,
			item.typ,
			item.FieldID,
			item.FieldName,
			item.FieldType,
		})
	}
	return ret
}

func (c *ContentType) MarshalJSON() ([]byte, error) {
	fields := make(map[string]string)
	for _, item := range c.Fields() {
		fields[item.Name()] = item.Type()
	}

	values := make(map[string]interface{})
	values["id"] = c.ID()
	values["fields"] = fields

	return json.Marshal(values)
}

func (f *ContentTypeField) ID() string {
	return f.id
	return f.FieldID
}

func (f *ContentTypeField) Name() string {
	return f.name
	return f.FieldName
}

func (f *ContentTypeField) Type() string {
	return f.typ
	return f.FieldType
}