~evanj/cms

f5488e05a5f521661d9d9d9f080b007595116a32 — Evan M Jones 8 months ago 742a26f
FIX(value tables): Each value type has its own table.
M internal/c/content/content.go => internal/c/content/content.go +27 -3
@@ 105,9 105,22 @@ func (c *Content) create(w http.ResponseWriter, r *http.Request) {
		if key == "space" || key == "contenttype" {
			continue
		}

		parts := strings.Split(key, "-")
		if len(parts) < 2 {
			c.Error(w, r, http.StatusInternalServerError, "invalid name field for value")
			return
		}

		typ, name, val := parts[0], parts[1], r.FormValue(key)
		if typ == valuetype.File {
			continue
		}

		params = append(params, db.ContentNewParam{
			Name:  key,
			Value: r.FormValue(key),
			Type:  typ,
			Name:  name,
			Value: val,
		})
	}



@@ 128,8 141,17 @@ func (c *Content) create(w http.ResponseWriter, r *http.Request) {
			return
		}

		parts := strings.Split(key, "-")
		if len(parts) < 2 {
			c.Error(w, r, http.StatusInternalServerError, "invalid name field for value")
			return
		}

		typ, name := parts[0], parts[1]

		params = append(params, db.ContentNewParam{
			Name:  key,
			Type:  typ,
			Name:  name,
			Value: url,
		})
	}


@@ 212,6 234,7 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {

		params = append(params, db.ContentUpdateParam{
			ID:    id,
			Type:  typ,
			Value: val,
		})
	}


@@ 243,6 266,7 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {

		params = append(params, db.ContentUpdateParam{
			ID:    id,
			Type:  valuetype.File,
			Value: url,
		})
	}

M internal/m/valuetype/valuetype.go => internal/m/valuetype/valuetype.go +2 -2
@@ 9,10 9,10 @@ const (
	InputMarkdown ValueTypeEnum = "InputMarkdown"
	File          ValueTypeEnum = "File"
	Date          ValueTypeEnum = "Date"
	Reference     ValueTypeEnum = "Reference"
	// Possible fields for the future.
	// FileList          ValueTypeEnum = "FileList"
	// Reference     ValueTypeEnum = "Reference"
	// ReferenceList ValueTypeEnum = "ReferenceList"
	// FileList      ValueTypeEnum = "FileList"
)

type ValueType interface {

M internal/s/db/content.go => internal/s/db/content.go +171 -19
@@ 8,6 8,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/value"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
)

type Content struct {


@@ 32,6 33,13 @@ const (
	`

	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 = ?;


@@ 53,39 61,136 @@ const (
	`

	queryValueNew = `
		INSERT INTO cms_value (CONTENT_ID, CONTENTTYPE_TO_VALUETYPE_ID, VALUE) 
		INSERT INTO cms_value (CONTENT_ID, CONTENTTYPE_TO_VALUETYPE_ID, VALUE_ID)
		VALUES (?, (SELECT ID FROM cms_contenttype_to_valuetype WHERE CONTENTTYPE_ID = ? AND NAME = ? LIMIT 1), ?);
	`

	queryValueUpdate = `
		UPDATE cms_value 
	queryValueNewStringSmall = `
		INSERT INTO cms_value_string_small (VALUE) 
		VALUES (?);
	`

	queryValueNewStringBig = `
		INSERT INTO cms_value_string_big (VALUE) 
		VALUES (?);
	`

	queryValueNewDate = `
		INSERT INTO cms_value_date (VALUE) 
		VALUES (?);
	`

	queryValueUpdateStringSmall = `
		UPDATE cms_value_string_small
		SET value = ?
		WHERE cms_value_string_small.ID IN ( SELECT VALUE_ID FROM cms_value WHERE cms_value.ID = ? );
	`

	queryValueUpdateStringBig = `
		UPDATE cms_value_string_big
		SET value = ?
		WHERE cms_value_string_big.ID IN ( SELECT VALUE_ID FROM cms_value WHERE cms_value.ID = ? );
	`

	queryValueUpdateDate = `
		UPDATE cms_value_date
		SET value = ?
		WHERE ID = ? 
		WHERE cms_value_date.ID IN ( SELECT VALUE_ID FROM cms_value WHERE cms_value.ID = ? );
	`

	queryValueGetByID = `
		SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value.VALUE
		FROM cms_value 
		JOIN cms_contenttype_to_valuetype 
	queryValueGetStringSmallByID = `
		SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_string_small.VALUE
		FROM cms_value
		JOIN cms_contenttype_to_valuetype
		ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
		JOIN cms_valuetype
		ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
		JOIN cms_value_string_small
		ON cms_value_string_small.ID = cms_value.VALUE_ID
		WHERE cms_value.ID = ?;
	`

	queryValueListByContent = `
		SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value.VALUE
		FROM cms_value 
		JOIN cms_contenttype_to_valuetype 
	queryValueGetStringBigByID = `
		SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_string_big.VALUE
		FROM cms_value
		JOIN cms_contenttype_to_valuetype
		ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
		JOIN cms_valuetype
		ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
		WHERE CONTENT_ID = ?
		ORDER BY cms_contenttype_to_valuetype.ID ASC;
		JOIN cms_value_string_big
		ON cms_value_string_big.ID = cms_value.VALUE_ID
		WHERE cms_value.ID = ?;
	`

	queryValueGetDateByID = `
		SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_date.VALUE
		FROM cms_value
		JOIN cms_contenttype_to_valuetype
		ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
		JOIN cms_valuetype
		ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
		JOIN cms_value_date
		ON cms_value_date.ID = cms_value.VALUE_ID
		WHERE cms_value.ID = ?;
	`

	queryValueListByContent = `
		SELECT ID, TYPE, NAME, VALUE FROM (

			SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_string_small.VALUE, cms_contenttype_to_valuetype.ID as ORDER_ID
			FROM cms_value
			JOIN cms_contenttype_to_valuetype
			ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
			JOIN cms_valuetype
			ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
			JOIN cms_value_string_small
			ON cms_value_string_small.ID = cms_value.VALUE_ID
			WHERE CONTENT_ID = ?
			AND (
				cms_valuetype.VALUE = 'StringSmall'
				OR cms_valuetype.VALUE = 'File'
			)

			UNION

			SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_date.VALUE, cms_contenttype_to_valuetype.ID as ORDER_ID
			FROM cms_value
			JOIN cms_contenttype_to_valuetype
			ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
			JOIN cms_valuetype
			ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
			JOIN cms_value_date
			ON cms_value_date.ID = cms_value.VALUE_ID
			WHERE CONTENT_ID = ?
			AND (
				cms_valuetype.VALUE = 'Date'
			)
			
			UNION

			SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_string_big.VALUE, cms_contenttype_to_valuetype.ID as ORDER_ID
			FROM cms_value
			JOIN cms_contenttype_to_valuetype
			ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
			JOIN cms_valuetype
			ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
			JOIN cms_value_string_big
			ON cms_value_string_big.ID = cms_value.VALUE_ID
			WHERE CONTENT_ID = ?
			AND (
				cms_valuetype.VALUE = 'StringBig'
				OR cms_valuetype.VALUE = 'InputHTML'
				OR cms_valuetype.VALUE = 'InputMarkdown'
			)

		)

		ORDER BY ORDER_ID ASC;
	`
)

type ContentNewParam struct {
	Type  string
	Name  string
	Value string
}


@@ 110,7 215,12 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [
	}

	for _, item := range params {
		res, err := db.Exec(queryValueNew, contentID, ct.ID(), item.Name, item.Value)
		queryValueNewType, queryValueGetTypeByID, _, err := db.valueQuerySetByType(item.Type)
		if err != nil {
			return nil, err
		}

		res, err := db.Exec(queryValueNewType, item.Value)
		if err != nil {
			// TODO cleanup orphan content.
			db.log.Println("db.Exec", err)


@@ 123,8 233,21 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [
			return nil, fmt.Errorf("failed to read new field value of content")
		}

		res, err = db.Exec(queryValueNew, contentID, ct.ID(), item.Name, valueID)
		if err != nil {
			// TODO cleanup orphan content.
			db.log.Println("db.Exec", err)
			return nil, fmt.Errorf("failed to attach field value of content")
		}

		valueID, err = res.LastInsertId()
		if err != nil {
			db.log.Println("res.LastInsertId", err)
			return nil, fmt.Errorf("failed to read new field value of content")
		}

		var value ContentValue
		if err := db.QueryRow(queryValueGetByID, valueID).Scan(&value.id, &value.typ, &value.name, &value.value); err != nil {
		if err := db.QueryRow(queryValueGetTypeByID, valueID).Scan(&value.id, &value.typ, &value.name, &value.value); err != nil {
			db.log.Println("db.QueryRow", err)
			return nil, fmt.Errorf("failed to find value created")
		}


@@ 141,11 264,16 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [

type ContentUpdateParam struct {
	ID    string
	Type  string
	Value string
}

func (db *DB) ContentUpdate(space space.Space, ct contenttype.ContentType, content content.Content, params []ContentUpdateParam) (content.Content, error) {
	for _, item := range params {
		_, _, queryValueUpdate, err := db.valueQuerySetByType(item.Type)
		if err != nil {
			return nil, err
		}
		if _, err := db.Exec(queryValueUpdate, item.Value, item.ID); err != nil {
			db.log.Println("db.Exec", err)
			return nil, fmt.Errorf("failed to create update content value '%s'", item.Value)


@@ 155,7 283,8 @@ 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 {
	_, err := db.Exec(queryContentDelete, content.ID(), content.ID())
	id := content.ID()
	_, err := db.Exec(queryContentDelete, id, id, id, id, id)
	return err
}



@@ 173,7 302,7 @@ func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentTyp
			return nil, err
		}

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


@@ 200,7 329,7 @@ func (db *DB) ContentGet(space space.Space, ct contenttype.ContentType, contentI
		return nil, fmt.Errorf("failed to find space")
	}

	rows, err := db.Query(queryValueListByContent, content.ID())
	rows, err := db.Query(queryValueListByContent, content.ID(), content.ID(), content.ID())
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find values(s)")


@@ 218,6 347,29 @@ func (db *DB) ContentGet(space space.Space, ct contenttype.ContentType, contentI
	return &content, nil
}

func (db *DB) valueQuerySetByType(typ valuetype.ValueTypeEnum) (insert, get, update string, err error) {
	switch typ {

	case valuetype.StringSmall:
		fallthrough
	case valuetype.File:
		return queryValueNewStringSmall, queryValueGetStringSmallByID, queryValueUpdateStringSmall, nil

	case valuetype.StringBig:
		fallthrough
	case valuetype.InputHTML:
		fallthrough
	case valuetype.InputMarkdown:
		return queryValueNewStringBig, queryValueGetStringBigByID, queryValueUpdateStringBig, nil

	case valuetype.Date:
		return queryValueNewDate, queryValueGetDateByID, queryValueUpdateDate, nil

	}

	return "", "", "", fmt.Errorf("%s is not a valid valuetype", typ)
}

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

M internal/s/db/db.go => internal/s/db/db.go +28 -3
@@ 115,17 115,42 @@ func (db *DB) EnsureSetup() error {
		);
	`)

	// value
	// content_to_value
	_, _ = db.Exec(`
		CREATE TABLE cms_value (
			ID INTEGER PRIMARY KEY AUTOINCREMENT,
			VALUE TEXT NOT NULL,
			CONTENT_ID INTEGER NOT NULL,
			CONTENTTYPE_TO_VALUETYPE_ID INTEGER NOT NULL,
			VALUE_ID INTEGER NOT NULL, -- Should be a foreign key but impossible to make it from three tables.
			FOREIGN KEY(CONTENT_ID) REFERENCES cms_content(ID)
			FOREIGN KEY(CONTENTTYPE_TO_VALUETYPE_ID) REFERENCES cms_contenttype_to_valuetype(ID)
		);
	`)

	// value StringSmall, File
	_, _ = db.Exec(`
		CREATE TABLE cms_value_string_small ( 
			ID INTEGER PRIMARY KEY AUTOINCREMENT,
			VALUE VARCHAR(256) NOT NULL
		);
	`)

	// value StringBig, InputHTML, InputMarkdown
	_, _ = db.Exec(`
		CREATE TABLE cms_value_string_big (
			ID INTEGER PRIMARY KEY AUTOINCREMENT,
			VALUE TEXT NOT NULL
		);
	`)

	// value Date
	_, _ = db.Exec(`
		CREATE TABLE cms_value_date (
			ID INTEGER PRIMARY KEY AUTOINCREMENT,
			VALUE DATETIME NOT NULL
		);
	`)

	// Only valuetypes cms supports.
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.StringSmall)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.StringBig)


@@ 133,7 158,7 @@ func (db *DB) EnsureSetup() error {
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.InputMarkdown)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.File)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.Date)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.Reference)
	// _, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.Reference)

	return nil
}

M internal/s/tmpl/html/content.html => internal/s/tmpl/html/content.html +0 -4
@@ 57,10 57,6 @@
              <input value="{{ .Value }}" required type=date name="{{ .Type }}-{{ .ID }}" placeholder="{{ .Name }}" />
            {{ end }}

            {{ if eq .Type "Reference" }}
              <input value="{{ .Value }}" required type=text name="{{ .Type }}-{{ .ID }}" placeholder="{{ .Name }}" />
            {{ end }}

          {{ end }}

          <input type=submit value=Go />

M internal/s/tmpl/html/contenttype.html => internal/s/tmpl/html/contenttype.html +6 -15
@@ 33,38 33,29 @@
            <label>{{ .Name }}</label>

            {{ if eq .Type "StringSmall" }}
              <input required type=text name="{{ .Name }}" placeholder="{{ .Name }}" />
              <input required type=text name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" />
            {{ end }}

            {{ if eq .Type "StringBig" }}
              <textarea required type=text name="{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
              <textarea required type=text name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
            {{ end }}

            {{ if eq .Type "InputHTML" }}
              <textarea class='input-html' required type=text name="{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
              <textarea class='input-html' required type=text name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
            {{ end }}

            {{ if eq .Type "InputMarkdown" }}
              <textarea class='input-markdown' required type=text name="{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
              <textarea class='input-markdown' required type=text name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
            {{ end }}

            {{ if eq .Type "File" }}
              <input required type=file name="{{ .Name }}" multiple=false />
              <input required type=file name="{{ .Type }}-{{ .Name }}" multiple=false />
            {{ end }}

            {{ if eq .Type "Date" }}
              <input required type=date name="{{ .Name }}" placeholder="{{ .Name }}" />
              <input required type=date name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" />
            {{ end }}

            {{ if eq .Type "Reference" }}
            <!-- use dialog for this popup menu 
              https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
            -->
              <input required type=text name="{{ .Name }}" placeholder="{{ .Name }}" />
            {{ end }}



          {{ end }}

          <input type=submit value=Go />

M internal/s/tmpl/html/space.html => internal/s/tmpl/html/space.html +0 -1
@@ 35,7 35,6 @@
              <option disabled value="InputMarkdown">Markdown</option>
              <option disabled value="File">File</option>
              <option disabled value="Date">Date</option>
              <option disabled value="Reference">Reference</option>
            </select>
          </div>
          <button id='add-fieldbtn'>Add Another Field</button>

M internal/s/tmpl/js/space.js => internal/s/tmpl/js/space.js +0 -1
@@ 18,7 18,6 @@
          <option value="InputMarkdown">Markdown</option>
          <option value="File">File</option>
          <option value="Date">Date</option>
          <option value="Reference">Reference</option>
        </select>
        <button id='remove-fieldbtn_${i}'>Remove Field</button>
      </div>

M internal/s/tmpl/tmpls_embed.go => internal/s/tmpl/tmpls_embed.go +6 -21
@@ 125,10 125,6 @@ textarea {
              <input value="{{ .Value }}" required type=date name="{{ .Type }}-{{ .ID }}" placeholder="{{ .Name }}" />
            {{ end }}

            {{ if eq .Type "Reference" }}
              <input value="{{ .Value }}" required type=text name="{{ .Type }}-{{ .ID }}" placeholder="{{ .Name }}" />
            {{ end }}

          {{ end }}

          <input type=submit value=Go />


@@ 192,38 188,29 @@ textarea {
            <label>{{ .Name }}</label>

            {{ if eq .Type "StringSmall" }}
              <input required type=text name="{{ .Name }}" placeholder="{{ .Name }}" />
              <input required type=text name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" />
            {{ end }}

            {{ if eq .Type "StringBig" }}
              <textarea required type=text name="{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
              <textarea required type=text name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
            {{ end }}

            {{ if eq .Type "InputHTML" }}
              <textarea class='input-html' required type=text name="{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
              <textarea class='input-html' required type=text name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
            {{ end }}

            {{ if eq .Type "InputMarkdown" }}
              <textarea class='input-markdown' required type=text name="{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
              <textarea class='input-markdown' required type=text name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" ></textarea>
            {{ end }}

            {{ if eq .Type "File" }}
              <input required type=file name="{{ .Name }}" multiple=false />
              <input required type=file name="{{ .Type }}-{{ .Name }}" multiple=false />
            {{ end }}

            {{ if eq .Type "Date" }}
              <input required type=date name="{{ .Name }}" placeholder="{{ .Name }}" />
              <input required type=date name="{{ .Type }}-{{ .Name }}" placeholder="{{ .Name }}" />
            {{ end }}

            {{ if eq .Type "Reference" }}
            <!-- use dialog for this popup menu 
              https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
            -->
              <input required type=text name="{{ .Name }}" placeholder="{{ .Name }}" />
            {{ end }}



          {{ end }}

          <input type=submit value=Go />


@@ 376,7 363,6 @@ textarea {
              <option disabled value="InputMarkdown">Markdown</option>
              <option disabled value="File">File</option>
              <option disabled value="Date">Date</option>
              <option disabled value="Reference">Reference</option>
            </select>
          </div>
          <button id='add-fieldbtn'>Add Another Field</button>


@@ 471,7 457,6 @@ textarea {
          <option value="InputMarkdown">Markdown</option>
          <option value="File">File</option>
          <option value="Date">Date</option>
          <option value="Reference">Reference</option>
        </select>
        <button id='remove-fieldbtn_${i}'>Remove Field</button>
      </div>