~evanj/cms

41bddf091e571b359b94d1f4287ccb471fb29d03 — Evan M Jones 5 months ago f4362a4 test/perPage/after
WIP(perPage/after): Testing to see if this is viable.
M internal/c/content/content.go => internal/c/content/content.go +5 -2
@@ 46,7 46,7 @@ type DBer interface {
	ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error)
	ContentUpdate(space space.Space, ct contenttype.ContentType, content content.Content, newParams []db.ContentNewParam, updateParams []db.ContentUpdateParam) (content.Content, error)
	ContentDelete(space space.Space, ct contenttype.ContentType, content content.Content) error
	ContentSearch(space space.Space, ct contenttype.ContentType, name, query string, before int) (content.ContentList, error)
	ContentSearch(space space.Space, ct contenttype.ContentType, name, query string, perPage, before, after int) (content.ContentList, error)
}

type E3er interface {


@@ 419,8 419,11 @@ func (c *Content) search(w http.ResponseWriter, r *http.Request) {
		return
	}

	perpage, _ := strconv.Atoi(r.URL.Query().Get("perpage"))
	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	list, err := c.db.ContentSearch(space, ct, field, query, before)
	after, _ := strconv.Atoi(r.URL.Query().Get("after"))

	list, err := c.db.ContentSearch(space, ct, field, query, perpage, before, after)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired content")

M internal/c/contenttype/contenttype.go => internal/c/contenttype/contenttype.go +10 -4
@@ 34,8 34,8 @@ type dber interface {
	ContentTypeGet(space space.Space, contenttypeID string) (contenttype.ContentType, error)
	ContentTypeUpdate(space space.Space, contenttype contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error)
	ContentTypeDelete(space space.Space, ct contenttype.ContentType) error
	ContentTypeSearch(space space.Space, query string, before int) (contenttype.ContentTypeList, error)
	ContentPerContentType(space space.Space, ct contenttype.ContentType, before int, order db.OrderType, sortField string) (content.ContentList, error)
	ContentTypeSearch(space space.Space, query string, perPage, before, after int) (contenttype.ContentTypeList, error)
	ContentPerContentType(space space.Space, ct contenttype.ContentType, perPage, before, after int, order db.OrderType, sortField string) (content.ContentList, error)
}

func New(log *log.Logger, db dber) *ContentType {


@@ 260,8 260,11 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request, spaceID, con
		f = "name" // All guaranteed to have.
	}

	perpage, _ := strconv.Atoi(r.URL.Query().Get("perpage"))
	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	list, err := c.db.ContentPerContentType(space, ct, before, o, f)
	after, _ := strconv.Atoi(r.URL.Query().Get("after"))

	list, err := c.db.ContentPerContentType(space, ct, perpage, before, after, o, f)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to find content for contenttype")


@@ 325,8 328,11 @@ func (c *ContentType) search(w http.ResponseWriter, r *http.Request) {
		return
	}

	perpage, _ := strconv.Atoi(r.URL.Query().Get("perpage"))
	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	list, err := c.db.ContentTypeSearch(space, query, before)
	after, _ := strconv.Atoi(r.URL.Query().Get("after"))

	list, err := c.db.ContentTypeSearch(space, query, perpage, before, after)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired contenttype")
		return

M internal/c/space/space.go => internal/c/space/space.go +10 -4
@@ 33,8 33,8 @@ type dber interface {
	SpaceUpdate(user user.User, space space.Space, name, desc string) (space.Space, error)
	SpaceCopy(user user.User, space space.Space, name, desc string) (space.Space, error)
	SpaceDelete(user user.User, space space.Space) error
	ContentTypesPerSpace(space space.Space, before int) (contenttype.ContentTypeList, error)
	HooksPerSpace(space space.Space, before int) (hook.HookList, error)
	ContentTypesPerSpace(space space.Space, perPage, before, after int) (contenttype.ContentTypeList, error)
	HooksPerSpace(space space.Space, perPage, before, after int) (hook.HookList, error)
}

func New(log *log.Logger, db dber) *Space {


@@ 58,15 58,21 @@ func (s *Space) serve(w http.ResponseWriter, r *http.Request, spaceID string) {
		return
	}

	perpagect, _ := strconv.Atoi(r.URL.Query().Get("perpagect"))
	beforect, _ := strconv.Atoi(r.URL.Query().Get("beforect"))
	cts, err := s.db.ContentTypesPerSpace(space, beforect)
	afterct, _ := strconv.Atoi(r.URL.Query().Get("afterct"))

	cts, err := s.db.ContentTypesPerSpace(space, perpagect, beforect, afterct)
	if err != nil {
		s.Error(w, r, http.StatusInternalServerError, "failed to find contenttypes for space")
		return
	}

	perpagehook, _ := strconv.Atoi(r.URL.Query().Get("perpagehook"))
	beforehook, _ := strconv.Atoi(r.URL.Query().Get("beforehook"))
	hooks, err := s.db.HooksPerSpace(space, beforehook)
	afterhook, _ := strconv.Atoi(r.URL.Query().Get("afterhook"))

	hooks, err := s.db.HooksPerSpace(space, perpagehook, beforehook, afterhook)
	if err != nil {
		s.log.Println(err)
		s.Error(w, r, http.StatusInternalServerError, "failed to find webhooks for space")

M internal/c/user/user.go => internal/c/user/user.go +4 -2
@@ 26,7 26,7 @@ type dber interface {
	UserNew(username, password, verifyPassword string) (user.User, error)
	UserGet(username, password string) (user.User, error)
	UserGetFromToken(token string) (user.User, error)
	SpacesPerUser(user user.User, before int) (space.SpaceList, error)
	SpacesPerUser(user user.User, perPage, before, after int) (space.SpaceList, error)
}

func New(log *log.Logger, db dber, signupEnabled bool) *User {


@@ 90,9 90,11 @@ func (l *User) home(w http.ResponseWriter, r *http.Request) {

	// Don't care about the error value here. When error occurs before is zero
	// value.
	perpage, _ := strconv.Atoi(r.URL.Query().Get("perpage"))
	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	after, _ := strconv.Atoi(r.URL.Query().Get("after"))

	spaces, err := l.db.SpacesPerUser(user, before)
	spaces, err := l.db.SpacesPerUser(user, perpage, before, after)
	if err != nil {
		l.Error(w, r, http.StatusInternalServerError, "failed to find spaces for user")
		return

M internal/m/content/content.go => internal/m/content/content.go +1 -0
@@ 16,4 16,5 @@ type ContentList interface {
	List() []Content
	More() bool
	Before() int // Some table ID to fetch next set of results.
	After() int
}

M internal/m/contenttype/contenttype.go => internal/m/contenttype/contenttype.go +1 -0
@@ 15,4 15,5 @@ type ContentTypeList interface {
	List() []ContentType
	More() bool
	Before() int
	After() int
}

M internal/m/hook/hook.go => internal/m/hook/hook.go +1 -0
@@ 9,4 9,5 @@ type HookList interface {
	List() []Hook
	More() bool
	Before() int
	After() int
}

M internal/m/space/space.go => internal/m/space/space.go +1 -0
@@ 10,4 10,5 @@ type SpaceList interface {
	List() []Space
	More() bool
	Before() int
	After() int
}

M internal/s/db/content.go => internal/s/db/content.go +33 -17
@@ 782,8 782,9 @@ func sortinfo(t *sql.Tx, ct contenttype.ContentType, sortField string) (string, 
	return sortFieldValueType, sortFieldTableName, nil
}

func (db *DB) contentPerContentType(t *sql.Tx, space space.Space, ct contenttype.ContentType, before int, order OrderType, sortField string, depth int) (content.ContentList, error) {
func (db *DB) contentPerContentType(t *sql.Tx, space space.Space, ct contenttype.ContentType, perPage, before, after int, order OrderType, sortField string, depth int) (content.ContentList, error) {
	var (
		firstID      int
		tmpID        int
		tmpContentID string



@@ 791,7 792,9 @@ func (db *DB) contentPerContentType(t *sql.Tx, space space.Space, ct contenttype
		hasMore bool
	)

	before = beformat(before)
	before = bfmt(before)
	after = afmt(after)
	perPage = pfmt(perPage)
	order = orderTypeReverse(order)

	// Create temporary table for queries.


@@ 838,8 841,8 @@ func (db *DB) contentPerContentType(t *sql.Tx, space space.Space, ct contenttype
	}

	// Query the temporary table.
	q = fmt.Sprintf("SELECT ID, CONTENT_ID FROM %s WHERE ID < ? ORDER BY ID DESC LIMIT ?", tbl)
	rows, err := t.Query(q, before, perPage+1)
	q = fmt.Sprintf("SELECT ID, CONTENT_ID FROM %s WHERE ID < ? AND ID > ? ORDER BY ID DESC LIMIT ?", tbl)
	rows, err := t.Query(q, before, after, perPage+1)
	if err != nil {
		return nil, err
	}


@@ 855,6 858,10 @@ func (db *DB) contentPerContentType(t *sql.Tx, space space.Space, ct contenttype
			return nil, err
		}

		if firstID == 0 {
			firstID = tmpID
		}

		c, err := db.ContentGet(space, ct, tmpContentID)
		if err != nil {
			return nil, err


@@ 863,17 870,17 @@ func (db *DB) contentPerContentType(t *sql.Tx, space space.Space, ct contenttype
		r = append(r, c)
	}

	return newContentList(r, hasMore, tmpID), nil
	return newContentList(r, hasMore, tmpID, firstID), nil
}

func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentType, before int, order OrderType, sortField string) (content.ContentList, error) {
func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentType, perPage, before, after int, order OrderType, sortField string) (content.ContentList, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	list, err := db.contentPerContentType(t, space, ct, before, order, sortField, defaultDepth)
	list, err := db.contentPerContentType(t, space, ct, perPage, before, after, order, sortField, defaultDepth)
	if err != nil {
		return nil, err
	}


@@ 881,8 888,9 @@ func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentTyp
	return list, t.Commit()
}

func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, sortField, query string, before int) (content.ContentList, error) {
func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, sortField, query string, perPage, before, after int) (content.ContentList, error) {
	var (
		firstID      int
		tmpID        int
		tmpContentID string



@@ 891,7 899,9 @@ func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, sortF
		hasMore bool
	)

	before = beformat(before)
	before = bfmt(before)
	after = afmt(after)
	perPage = pfmt(perPage)

	t, err := db.Begin()
	if err != nil {


@@ 942,8 952,8 @@ func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, sortF
	}

	// Query the temporary table.
	q = fmt.Sprintf("SELECT ID, CONTENT_ID FROM %s WHERE ID < ? ORDER BY ID DESC LIMIT ?", tbl)
	rows, err := t.Query(q, before, perPage+1)
	q = fmt.Sprintf("SELECT ID, CONTENT_ID FROM %s WHERE ID < ? AND ID > ? ORDER BY ID DESC LIMIT ?", tbl)
	rows, err := t.Query(q, before, after, perPage+1)
	if err != nil {
		return nil, err
	}


@@ 959,6 969,10 @@ func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, sortF
			return nil, err
		}

		if firstID == 0 {
			firstID = tmpID
		}

		c, err := db.ContentGet(space, ct, tmpContentID)
		if err != nil {
			return nil, err


@@ 967,7 981,7 @@ func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, sortF
		r = append(r, c)
	}

	return newContentList(r, hasMore, tmpID), t.Commit()
	return newContentList(r, hasMore, tmpID, firstID), t.Commit()
}

func (db *DB) contentGet(t *sql.Tx, space space.Space, ct contenttype.ContentType, contentID string, depth int) (content.Content, error) {


@@ 1248,7 1262,7 @@ type contentIter struct {
}

func (db *DB) contentIter(t *sql.Tx, space space.Space, ct contenttype.ContentType, sortField string) *contentIter {
	iter := &contentIter{db, t, space, ct, sortField, newContentList(nil, false, 0), nil}
	iter := &contentIter{db, t, space, ct, sortField, newContentList(nil, false, 0, 0), nil}
	iter.pump()
	return iter
}


@@ 1262,7 1276,7 @@ func (db *DB) ContentIter(space space.Space, ct contenttype.ContentType, sortFie
}

func (iter *contentIter) pump() {
	list, err := iter.db.contentPerContentType(iter.t, iter.space, iter.ct, iter.list.Before(), OrderAsc, iter.sortField, defaultDepth)
	list, err := iter.db.contentPerContentType(iter.t, iter.space, iter.ct, maxPerPage, iter.list.Before(), iter.list.After(), OrderAsc, iter.sortField, defaultDepth)
	iter.list = list
	iter.err = err
}


@@ 1287,7 1301,7 @@ func (iter *contentIter) Scan() (content.Content, error) {
	list := iter.list.List()
	first, rest := list[0], list[1:]

	iter.list = newContentList(rest, iter.list.More(), iter.list.Before())
	iter.list = newContentList(rest, iter.list.More(), iter.list.Before(), iter.list.Before())
	if err != nil {
		return nil, err
	}


@@ 1305,12 1319,14 @@ type ContentList struct {
	ContentList       []content.Content
	ContentListMore   bool
	ContentListBefore int
	ContentListAfter  int
}

func newContentList(list []content.Content, hasMore bool, last int) *ContentList {
	return &ContentList{list, hasMore, last}
func newContentList(list []content.Content, hasMore bool, last, first int) *ContentList {
	return &ContentList{list, hasMore, last, first}
}

func (cl *ContentList) List() []content.Content { return cl.ContentList }
func (cl *ContentList) More() bool              { return cl.ContentListMore }
func (cl *ContentList) Before() int             { return cl.ContentListBefore }
func (cl *ContentList) After() int              { return cl.ContentListAfter }

M internal/s/db/contenttype.go => internal/s/db/contenttype.go +33 -17
@@ 167,23 167,26 @@ func (db *DB) ContentTypeUpdate(space space.Space, contenttype contenttype.Conte
	return db.ContentTypeGet(space, contenttype.ID())
}

func (db *DB) contentTypesPerSpace(t *sql.Tx, space space.Space, before int) (contenttype.ContentTypeList, error) {
func (db *DB) contentTypesPerSpace(t *sql.Tx, space space.Space, perPage, before, after int) (contenttype.ContentTypeList, error) {
	var (
		r       []contenttype.ContentType
		first   int
		id      int
		hasMore bool
	)

	before = beformat(before)
	before = bfmt(before)
	after = afmt(after)
	perPage = pfmt(perPage)

	q := `
		SELECT ID 
		FROM cms_contenttype
		WHERE SPACE_ID = ? AND ID < ?
		WHERE SPACE_ID = ? AND ID < ? AND ID > ?
		ORDER BY ID DESC LIMIT ?
	`

	rows, err := db.Query(q, space.ID(), before, perPage+1)
	rows, err := db.Query(q, space.ID(), before, after, perPage+1)
	if err != nil {
		return nil, err
	}


@@ 198,6 201,10 @@ func (db *DB) contentTypesPerSpace(t *sql.Tx, space space.Space, before int) (co
			return nil, err
		}

		if first == 0 {
			first = id
		}

		ct, err := db.contentTypeGet(t, space, strconv.Itoa(id))
		if err != nil {
			return nil, err


@@ 206,17 213,17 @@ func (db *DB) contentTypesPerSpace(t *sql.Tx, space space.Space, before int) (co
		r = append(r, ct)
	}

	return newContentTypeList(r, hasMore, id), nil
	return newContentTypeList(r, hasMore, id, first), nil
}

func (db *DB) ContentTypesPerSpace(space space.Space, before int) (contenttype.ContentTypeList, error) {
func (db *DB) ContentTypesPerSpace(space space.Space, perPage, before, after int) (contenttype.ContentTypeList, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	list, err := db.contentTypesPerSpace(t, space, before)
	list, err := db.contentTypesPerSpace(t, space, perPage, before, after)
	if err != nil {
		db.log.Println(err)
		return nil, err


@@ 263,19 270,22 @@ func (db *DB) ContentTypeGet(space space.Space, contenttypeID string) (contentty

// TODO: Consolidate with other list function here. They are the same except for
// the query used.
func (db *DB) ContentTypeSearch(space space.Space, query string, before int) (contenttype.ContentTypeList, error) {
func (db *DB) ContentTypeSearch(space space.Space, query string, perPage, before, after int) (contenttype.ContentTypeList, error) {
	var (
		r       []contenttype.ContentType
		first   int
		id      int
		hasMore bool
	)

	before = beformat(before)
	before = bfmt(before)
	after = afmt(after)
	perPage = pfmt(perPage)

	// TODO: May want to make a temp table for this query for proper ordering.
	q := `SELECT ID FROM cms_contenttype WHERE NAME LIKE ? AND SPACE_ID = ? AND ID < ? ORDER BY ID DESC LIMIT ?`
	q := `SELECT ID FROM cms_contenttype WHERE NAME LIKE ? AND SPACE_ID = ? AND ID < ?  AND ID > ? ORDER BY ID DESC LIMIT ?`

	rows, err := db.Query(q, fmt.Sprintf("%%%s%%", query), space.ID(), before, perPage)
	rows, err := db.Query(q, fmt.Sprintf("%%%s%%", query), space.ID(), before, after, perPage)
	if err != nil {
		db.log.Println("1", err)
		return nil, err


@@ 287,6 297,10 @@ func (db *DB) ContentTypeSearch(space space.Space, query string, before int) (co
			return nil, err
		}

		if first == 0 {
			first = id
		}

		ct, err := db.ContentTypeGet(space, strconv.Itoa(id))
		if err != nil {
			db.log.Println("3", err)


@@ 296,7 310,7 @@ func (db *DB) ContentTypeSearch(space space.Space, query string, before int) (co
		r = append(r, ct)
	}

	return newContentTypeList(r, hasMore, id), nil
	return newContentTypeList(r, hasMore, id, first), nil
}

func (db *DB) ContentTypeDelete(space space.Space, ct contenttype.ContentType) error {


@@ 368,13 382,13 @@ type contentTypeIter struct {
}

func (db *DB) ContentTypeIter(t *sql.Tx, space space.Space) *contentTypeIter {
	iter := &contentTypeIter{db, t, space, newContentTypeList(nil, false, 0), nil}
	iter := &contentTypeIter{db, t, space, newContentTypeList(nil, false, 0, 0), nil}
	iter.pump()
	return iter
}

func (iter *contentTypeIter) pump() {
	list, err := iter.db.contentTypesPerSpace(iter.t, iter.space, iter.list.Before())
	list, err := iter.db.contentTypesPerSpace(iter.t, iter.space, maxPerPage, iter.list.Before(), iter.list.After())
	iter.list = list
	iter.err = err
}


@@ 399,7 413,7 @@ func (iter *contentTypeIter) Scan() (contenttype.ContentType, error) {
	list := iter.list.List()
	first, rest := list[0], list[1:]

	iter.list = newContentTypeList(rest, iter.list.More(), iter.list.Before())
	iter.list = newContentTypeList(rest, iter.list.More(), iter.list.Before(), iter.list.After())
	if err != nil {
		return nil, err
	}


@@ 417,12 431,14 @@ type ContentTypeList struct {
	ContentTypeList       []contenttype.ContentType
	ContentTypeListMore   bool
	ContentTypeListBefore int
	ContentTypeListAfter  int
}

func newContentTypeList(list []contenttype.ContentType, hasMore bool, last int) *ContentTypeList {
	return &ContentTypeList{list, hasMore, last}
func newContentTypeList(list []contenttype.ContentType, hasMore bool, last, first int) *ContentTypeList {
	return &ContentTypeList{list, hasMore, last, first}
}

func (ctl *ContentTypeList) List() []contenttype.ContentType { return ctl.ContentTypeList }
func (ctl *ContentTypeList) More() bool                      { return ctl.ContentTypeListMore }
func (ctl *ContentTypeList) Before() int                     { return ctl.ContentTypeListBefore }
func (ctl *ContentTypeList) After() int                      { return ctl.ContentTypeListAfter }

M internal/s/db/db.go => internal/s/db/db.go +13 -2
@@ 13,7 13,7 @@ import (

const (
	// Default pagination amount. For use in LIMIT/OFFSET.
	perPage = 25
	maxPerPage = 25

	maxUint = ^uint(0)
	maxInt  = int(maxUint >> 1)


@@ 26,13 26,24 @@ var (
	zero      int
)

func beformat(before int) int {
func bfmt(before int) int {
	if before == zero {
		return maxInt
	}
	return before
}

func afmt(after int) int {
	return after
}

func pfmt(page int) int {
	if page > maxPerPage || page < 1 {
		return maxPerPage
	}
	return page
}

type DB struct {
	*sql.DB
	log *log.Logger

M internal/s/db/hook.go => internal/s/db/hook.go +21 -12
@@ 93,22 93,25 @@ func (db *DB) HookDelete(s space.Space, h hook.Hook) error {
	return t.Commit()
}

func (db *DB) hooksPerSpace(t *sql.Tx, space space.Space, before int) (hook.HookList, error) {
func (db *DB) hooksPerSpace(t *sql.Tx, space space.Space, perPage, before, after int) (hook.HookList, error) {
	var (
		r       []hook.Hook
		first   int
		id      int
		hasMore bool
	)

	before = beformat(before)
	before = bfmt(before)
	after = afmt(after)
	perPage = pfmt(perPage)

	q := `
		SELECT ID FROM cms_hooks
		WHERE SPACE_ID = ? AND ID < ?
		WHERE SPACE_ID = ? AND ID < ? AND ID > ?
		ORDER BY ID DESC LIMIT ?
	 `

	rows, err := db.Query(q, space.ID(), before, perPage+1)
	rows, err := db.Query(q, space.ID(), before, after, perPage+1)
	if err != nil {
		return nil, err
	}


@@ 123,6 126,10 @@ func (db *DB) hooksPerSpace(t *sql.Tx, space space.Space, before int) (hook.Hook
			return nil, err
		}

		if first == 0 {
			first = id
		}

		ct, err := db.hookGet(t, space, strconv.Itoa(id))
		if err != nil {
			return nil, err


@@ 131,17 138,17 @@ func (db *DB) hooksPerSpace(t *sql.Tx, space space.Space, before int) (hook.Hook
		r = append(r, ct)
	}

	return newHookList(r, hasMore, id), nil
	return newHookList(r, hasMore, id, first), nil
}

func (db *DB) HooksPerSpace(space space.Space, before int) (hook.HookList, error) {
func (db *DB) HooksPerSpace(space space.Space, perPage, before, after int) (hook.HookList, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	list, err := db.hooksPerSpace(t, space, before)
	list, err := db.hooksPerSpace(t, space, perPage, before, after)
	if err != nil {
		db.log.Println(err)
		return nil, err


@@ 168,13 175,13 @@ type hookIter struct {
}

func (db *DB) HookIter(t *sql.Tx, space space.Space) *hookIter {
	iter := &hookIter{db, t, space, newHookList(nil, false, 0), nil}
	iter := &hookIter{db, t, space, newHookList(nil, false, 0, 0), nil}
	iter.pump()
	return iter
}

func (iter *hookIter) pump() {
	list, err := iter.db.hooksPerSpace(iter.t, iter.space, iter.list.Before())
	list, err := iter.db.hooksPerSpace(iter.t, iter.space, maxPerPage, iter.list.Before(), iter.list.After())
	iter.list = list
	iter.err = err
}


@@ 199,7 206,7 @@ func (iter *hookIter) Scan() (hook.Hook, error) {
	list := iter.list.List()
	first, rest := list[0], list[1:]

	iter.list = newHookList(rest, iter.list.More(), iter.list.Before())
	iter.list = newHookList(rest, iter.list.More(), iter.list.Before(), iter.list.After())
	if err != nil {
		return nil, err
	}


@@ 217,12 224,14 @@ type HookList struct {
	HookList       []hook.Hook
	HookListMore   bool
	HookListBefore int
	HookListAfter  int
}

func newHookList(list []hook.Hook, hasMore bool, last int) *HookList {
	return &HookList{list, hasMore, last}
func newHookList(list []hook.Hook, hasMore bool, last, first int) *HookList {
	return &HookList{list, hasMore, last, first}
}

func (hl *HookList) List() []hook.Hook { return hl.HookList }
func (hl *HookList) More() bool        { return hl.HookListMore }
func (hl *HookList) Before() int       { return hl.HookListBefore }
func (hl *HookList) After() int        { return hl.HookListAfter }

M internal/s/db/space.go => internal/s/db/space.go +18 -9
@@ 341,22 341,25 @@ func (db *DB) SpaceDelete(user user.User, space space.Space) error {
	return t.Commit()
}

func (db *DB) spacesPerUser(t *sql.Tx, user user.User, before int) (space.SpaceList, error) {
func (db *DB) spacesPerUser(t *sql.Tx, user user.User, perPage, before, after int) (space.SpaceList, error) {
	var (
		r       []space.Space
		first   int
		id      int
		hasMore bool
	)

	before = beformat(before)
	before = bfmt(before)
	after = afmt(after)
	perPage = pfmt(perPage)

	q := `
		SELECT SPACE_ID FROM cms_user_to_space
		WHERE USER_ID = ? AND SPACE_ID < ?
		WHERE USER_ID = ? AND SPACE_ID < ? AND SPACE_ID > ?
		ORDER BY SPACE_ID DESC LIMIT ?
	`

	rows, err := db.Query(q, user.ID(), before, perPage+1)
	rows, err := db.Query(q, user.ID(), before, after, perPage+1)
	if err != nil {
		return nil, err
	}


@@ 371,6 374,10 @@ func (db *DB) spacesPerUser(t *sql.Tx, user user.User, before int) (space.SpaceL
			return nil, err
		}

		if first == 0 {
			first = id
		}

		s, err := db.spaceGet(t, user, strconv.Itoa(id))
		if err != nil {
			return nil, err


@@ 379,17 386,17 @@ func (db *DB) spacesPerUser(t *sql.Tx, user user.User, before int) (space.SpaceL
		r = append(r, s)
	}

	return newSpaceList(r, hasMore, id), nil
	return newSpaceList(r, hasMore, id, first), nil
}

func (db *DB) SpacesPerUser(user user.User, before int) (space.SpaceList, error) {
func (db *DB) SpacesPerUser(user user.User, perPage, before, after int) (space.SpaceList, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	list, err := db.spacesPerUser(t, user, before)
	list, err := db.spacesPerUser(t, user, perPage, before, after)
	if err != nil {
		db.log.Println(err)
		return nil, err


@@ 416,12 423,14 @@ type SpaceList struct {
	SpaceList       []space.Space
	SpaceListMore   bool
	SpaceListBefore int
	SpaceListAfter  int
}

func newSpaceList(list []space.Space, hasMore bool, last int) *SpaceList {
	return &SpaceList{list, hasMore, last}
func newSpaceList(list []space.Space, hasMore bool, first, last int) *SpaceList {
	return &SpaceList{list, hasMore, last, first}
}

func (sl *SpaceList) List() []space.Space { return sl.SpaceList }
func (sl *SpaceList) More() bool          { return sl.SpaceListMore }
func (sl *SpaceList) Before() int         { return sl.SpaceListBefore }
func (sl *SpaceList) After() int          { return sl.SpaceListAfter }

M internal/s/hook/hook.go => internal/s/hook/hook.go +9 -7
@@ 32,7 32,7 @@ type Hook struct {
}

type dber interface {
	HooksPerSpace(space space.Space, before int) (hook.HookList, error)
	HooksPerSpace(space space.Space, perPage, before, after int) (hook.HookList, error)
}

func New(log *log.Logger, db dber) *Hook {


@@ 79,18 79,20 @@ func (h *Hook) do(ctx context.Context, content content.Content, hook hook.Hook, 

func (h *Hook) Do(space space.Space, content content.Content, ht HookType) {
	var (
		hooks  hook.HookList
		before int
		err    error
		eg     errgroup.Group
		ctx, _ = context.WithTimeout(
		hooks   hook.HookList
		perPage int
		after   int
		before  int
		err     error
		eg      errgroup.Group
		ctx, _  = context.WithTimeout(
			context.Background(),
			10*time.Second, // TODO: May want to lower?
		)
	)

	for i := 0; i == 0 || hooks.More(); i++ {
		hooks, err = h.db.HooksPerSpace(space, before)
		hooks, err = h.db.HooksPerSpace(space, perPage, before, after)
		if err != nil {
			h.log.Println("failed to find webhooks for", space.ID(), space.Name(), err)
			return