Skip to content

Commit f80ad4a

Browse files
committed
Add Hooks
1 parent 7ce9025 commit f80ad4a

File tree

7 files changed

+214
-1
lines changed

7 files changed

+214
-1
lines changed

create.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"fmt"
66
)
77

8+
// OnCreateHook is the signature of [Hooks.OnCreate] hook.
9+
type OnCreateHook func(*HookContext, any, *CreateOptions)
10+
811
// CreateOptions are options for the `Create` operation.
912
type CreateOptions struct {
1013
CreateMetadata Metadata
@@ -28,6 +31,12 @@ type CreateResult struct{}
2831
//
2932
// See `CreateOptions` for replacing documents if already present.
3033
func (s *Store[T]) Create(ctx context.Context, doc *T, opts CreateOptions) (*CreateResult, error) {
34+
hookCtx := NewHookContext(ctx, s)
35+
for _, hook := range s.hooks {
36+
if hook.OnCreate != nil {
37+
hook.OnCreate(hookCtx, doc, &opts)
38+
}
39+
}
3140
id := s.idFn(doc)
3241
if id == "" {
3342
return nil, fmt.Errorf("nidhi: id cannot be empty")

delete.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import (
77
sq "github.com/elgris/sqrl"
88
)
99

10+
// OnDeleteHook is the function signature for the [Hooks.OnDelete] hook.
11+
type OnDeleteHook func(*HookContext, string, *DeleteOptions)
12+
13+
// OnDeleteManyHook is the function signature for the [Hooks.OnDeleteMany] hook.
14+
type OnDeleteManyHook func(*HookContext, Sqlizer, *DeleteManyOptions)
15+
1016
// DeleteOptions are options for the `Delete` operation
1117
type DeleteOptions struct {
1218
// Metadata is the metadata of the document.
@@ -45,6 +51,12 @@ type DeleteManyResult struct {
4551
//
4652
// By default all deletes are soft deletes. To hard delete, see `DeleteOptions`
4753
func (s *Store[T]) Delete(ctx context.Context, id string, opts DeleteOptions) (*DeleteResult, error) {
54+
hookCtx := NewHookContext(ctx, s)
55+
for _, h := range s.hooks {
56+
if h.OnDelete != nil {
57+
h.OnDelete(hookCtx, id, &opts)
58+
}
59+
}
4860
var (
4961
sqlStr string
5062
args []any
@@ -79,6 +91,12 @@ func (s *Store[T]) Delete(ctx context.Context, id string, opts DeleteOptions) (*
7991
}
8092

8193
func (s *Store[T]) DeleteMany(ctx context.Context, q Sqlizer, opts DeleteManyOptions) (*DeleteManyResult, error) {
94+
hookCtx := NewHookContext(ctx, s)
95+
for _, h := range s.hooks {
96+
if h.OnDeleteMany != nil {
97+
h.OnDeleteMany(hookCtx, q, &opts)
98+
}
99+
}
82100
var (
83101
sqlStr string
84102
args []any

get.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import (
1010
"golang.org/x/exp/maps"
1111
)
1212

13+
// OnGetHook is the signature of [Hooks.OnGet] hook.
14+
type OnGetHook func(*HookContext, string, *GetOptions)
15+
16+
// OnQueryHook is the signature of [Hooks.OnQuery] hook.
17+
type OnQueryHook func(*HookContext, Sqlizer, *QueryOptions)
18+
1319
// Document is wrapper for a resource.
1420
type Document[T any] struct {
1521
// Value is the resource.
@@ -91,6 +97,12 @@ type QueryResult[T any] struct {
9197

9298
// Get is used to get a document from the store.
9399
func (s *Store[T]) Get(ctx context.Context, id string, opts GetOptions) (*GetResult[T], error) {
100+
hookCtx := NewHookContext(ctx, s)
101+
for _, h := range s.hooks {
102+
if h.OnGet != nil {
103+
h.OnGet(hookCtx, id, &opts)
104+
}
105+
}
94106
var selection any = ColDoc
95107
if len(s.fields) > 0 && len(opts.ViewMask) > 0 {
96108
selection = sq.Expr(ColDoc+" - ?::text[]", pg.Array(difference(s.fields, opts.ViewMask)))
@@ -130,6 +142,12 @@ func (s *Store[T]) Get(ctx context.Context, id string, opts GetOptions) (*GetRes
130142

131143
// Query queries the store and returns all matching documents.
132144
func (s *Store[T]) Query(ctx context.Context, q Sqlizer, opts QueryOptions) (*QueryResult[T], error) {
145+
hookCtx := NewHookContext(ctx, s)
146+
for _, h := range s.hooks {
147+
if h.OnQuery != nil {
148+
h.OnQuery(hookCtx, q, &opts)
149+
}
150+
}
133151
selection := any(ColDoc)
134152
if len(s.fields) > 0 && len(opts.ViewMask) > 0 {
135153
selection = sq.Expr(ColDoc+" - ?::text[]", pg.Array(difference(s.fields, opts.ViewMask)))

hooks.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package nidhi
2+
3+
import "context"
4+
5+
// Hooks are callbacks that are called on store operations.
6+
//
7+
// The passed values can if modified will change for the op.
8+
type Hooks struct {
9+
// OnCreate is called on [Store.Create].
10+
OnCreate OnCreateHook
11+
// OnGet is called on [Store.Get].
12+
OnGet OnGetHook
13+
// OnQuery is called on [Store.Query].
14+
OnQuery OnQueryHook
15+
// OnDelete is called on [Store.Delete].
16+
OnDelete OnDeleteHook
17+
// OnDeleteMany is called on [Store.DeleteMany].
18+
OnDeleteMany OnDeleteManyHook
19+
// OnReplace is called on [Store.Replace].
20+
OnReplace OnReplaceHook
21+
// OnUpdate is called on [Store.Update].
22+
OnUpdate OnUpdateHook
23+
// OnUpdateMany is called on [Store.UpdateMany].
24+
OnUpdateMany OnUpdateManyHook
25+
}
26+
27+
// HookContext is the context passed to [Hooks]
28+
type HookContext struct {
29+
context.Context
30+
31+
idFn func(any) string
32+
setIdFn func(any, string)
33+
}
34+
35+
// NewHookContext returns a [HookContext].
36+
func NewHookContext[T any](ctx context.Context, store *Store[T]) *HookContext {
37+
return &HookContext{
38+
Context: ctx,
39+
idFn: func(t any) string {
40+
return store.idFn(t.(*T))
41+
},
42+
setIdFn: func(t any, s string) {
43+
store.setIdFn(t.(*T), s)
44+
},
45+
}
46+
}
47+
48+
// SetId sets the id field of the document.
49+
func (s *HookContext) SetId(v any, id string) {
50+
s.setIdFn(v, id)
51+
}
52+
53+
// Id returns the id of the document
54+
func (s *HookContext) Id(v any) string {
55+
return s.idFn(v)
56+
}

hooks_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package nidhi_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/akshayjshah/attest"
8+
"github.com/srikrsna/nidhi"
9+
)
10+
11+
func TestHooks(t *testing.T) {
12+
db := newDB(t)
13+
const (
14+
create int = iota
15+
get
16+
query
17+
replace
18+
update
19+
updateMany
20+
delete
21+
deleteMany
22+
)
23+
called := map[int]bool{}
24+
store := newStore(t, db, nidhi.StoreOptions{
25+
Hooks: []nidhi.Hooks{
26+
{
27+
OnCreate: func(*nidhi.HookContext, any, *nidhi.CreateOptions) { called[create] = true },
28+
OnGet: func(*nidhi.HookContext, string, *nidhi.GetOptions) { called[get] = true },
29+
OnQuery: func(*nidhi.HookContext, nidhi.Sqlizer, *nidhi.QueryOptions) { called[query] = true },
30+
OnDelete: func(*nidhi.HookContext, string, *nidhi.DeleteOptions) { called[delete] = true },
31+
OnDeleteMany: func(*nidhi.HookContext, nidhi.Sqlizer, *nidhi.DeleteManyOptions) { called[deleteMany] = true },
32+
OnReplace: func(*nidhi.HookContext, any, *nidhi.ReplaceOptions) { called[replace] = true },
33+
OnUpdate: func(*nidhi.HookContext, string, any, *nidhi.UpdateOptions) { called[update] = true },
34+
OnUpdateMany: func(*nidhi.HookContext, any, nidhi.Sqlizer, *nidhi.UpdateManyOptions) { called[updateMany] = true },
35+
},
36+
},
37+
})
38+
ctx := context.TODO()
39+
_, _ = store.Create(ctx, defaultResource(), nidhi.CreateOptions{})
40+
attest.True(t, called[create])
41+
_, _ = store.Get(ctx, "", nidhi.GetOptions{})
42+
attest.True(t, called[get])
43+
_, _ = store.Query(ctx, nil, nidhi.QueryOptions{})
44+
attest.True(t, called[query])
45+
_, _ = store.Delete(ctx, "", nidhi.DeleteOptions{})
46+
attest.True(t, called[delete])
47+
_, _ = store.DeleteMany(ctx, nil, nidhi.DeleteManyOptions{})
48+
attest.True(t, called[deleteMany])
49+
_, _ = store.Replace(ctx, defaultResource(), nidhi.ReplaceOptions{})
50+
attest.True(t, called[replace])
51+
_, _ = store.Update(ctx, "", nil, nidhi.UpdateOptions{})
52+
attest.True(t, called[update])
53+
_, _ = store.UpdateMany(ctx, nil, nil, nidhi.UpdateManyOptions{})
54+
attest.True(t, called[updateMany])
55+
}
56+
57+
func TestHookContext(t *testing.T) {
58+
var (
59+
idFn = func(t *resource) string { return t.Id }
60+
setIdFn = func(t *resource, s string) { t.Id = s }
61+
)
62+
db := newDB(t)
63+
store, err := nidhi.NewStore(
64+
context.Background(),
65+
db,
66+
"schema",
67+
"table",
68+
[]string{"field"},
69+
idFn,
70+
setIdFn,
71+
nidhi.StoreOptions{},
72+
)
73+
attest.Ok(t, err)
74+
attest.NotZero(t, store)
75+
ctx := nidhi.NewHookContext(context.Background(), store)
76+
r := defaultResource()
77+
r.Id = "id"
78+
attest.Equal(t, ctx.Id(r), r.Id)
79+
ctx.SetId(r, "new")
80+
attest.Equal(t, r.Id, "new")
81+
}

store.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type (
4141
type StoreOptions struct {
4242
// MetadataRegistry is the registry of metadata parts.
4343
MetadataRegistry map[string]func() MetadataPart
44+
// Hooks for the store operations.
45+
Hooks []Hooks
4446
}
4547

4648
// Store is the collection of documents
@@ -53,7 +55,8 @@ type Store[T any] struct {
5355
idFn IdFn[T]
5456
setIdFn SetIdFn[T]
5557

56-
mdr map[string]func() MetadataPart
58+
mdr map[string]func() MetadataPart
59+
hooks []Hooks
5760
}
5861

5962
// NewStore returns a new store.
@@ -82,5 +85,6 @@ func NewStore[T any](
8285
idFn: idFn,
8386
setIdFn: setIdFn,
8487
mdr: opts.MetadataRegistry,
88+
hooks: opts.Hooks,
8589
}, nil
8690
}

update.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import (
77
sq "github.com/elgris/sqrl"
88
)
99

10+
// OnReplaceHook is the signature for the [Hooks.OnReplace] hook.
11+
type OnReplaceHook func(*HookContext, any, *ReplaceOptions)
12+
13+
// OnUpdateHook is the signature for the [Hooks.OnUpdate] hook.
14+
type OnUpdateHook func(*HookContext, string, any, *UpdateOptions)
15+
16+
// OnUpdateManyHook is the signature for the [Hooks.OnUpdateMany] hook.
17+
type OnUpdateManyHook func(*HookContext, any, Sqlizer, *UpdateManyOptions)
18+
1019
// ReplaceOptions are options for `Replace` operation.
1120
type ReplaceOptions struct {
1221
// Metadata is the metadata of the document.
@@ -54,6 +63,12 @@ type UpdateManyResult struct {
5463
//
5564
// Returns a NotFound error, if the document doesn't exist or the revision doesn't exist.
5665
func (s *Store[T]) Replace(ctx context.Context, doc *T, opts ReplaceOptions) (*ReplaceResult, error) {
66+
hookCtx := NewHookContext(ctx, s)
67+
for _, h := range s.hooks {
68+
if h.OnReplace != nil {
69+
h.OnReplace(hookCtx, doc, &opts)
70+
}
71+
}
5772
rc, err := s.update(ctx, doc, sq.Eq{ColId: s.idFn(doc)}, false, opts.Metadata, opts.Revision)
5873
if err != nil {
5974
return nil, err
@@ -68,6 +83,12 @@ func (s *Store[T]) Replace(ctx context.Context, doc *T, opts ReplaceOptions) (*R
6883
//
6984
// Returns a NotFound error on id and revision mismatch.
7085
func (s *Store[T]) Update(ctx context.Context, id string, updates any, opts UpdateOptions) (*UpdateResult, error) {
86+
hookCtx := NewHookContext(ctx, s)
87+
for _, h := range s.hooks {
88+
if h.OnUpdate != nil {
89+
h.OnUpdate(hookCtx, id, updates, &opts)
90+
}
91+
}
7192
rc, err := s.update(ctx, updates, sq.Eq{ColId: id}, true, opts.Metadata, opts.Revision)
7293
if err != nil {
7394
return nil, err
@@ -80,6 +101,12 @@ func (s *Store[T]) Update(ctx context.Context, id string, updates any, opts Upda
80101

81102
// UpdateMany updates all the documents that match the given query.
82103
func (s *Store[T]) UpdateMany(ctx context.Context, updates any, q Sqlizer, opts UpdateManyOptions) (*UpdateManyResult, error) {
104+
hookCtx := NewHookContext(ctx, s)
105+
for _, h := range s.hooks {
106+
if h.OnUpdateMany != nil {
107+
h.OnUpdateMany(hookCtx, updates, q, &opts)
108+
}
109+
}
83110
rc, err := s.update(ctx, updates, q, true, opts.Metadata, -1)
84111
if err != nil {
85112
return nil, err

0 commit comments

Comments
 (0)