From 579f8bf0aa62299e10eb3a4f18376b3d6b696635 Mon Sep 17 00:00:00 2001 From: WinPooh32 Date: Thu, 4 Jan 2024 17:09:55 +1000 Subject: [PATCH] up --- .github/workflows/test.yml | 21 ++ LICENSE | 21 ++ README.md | 15 + driver/sql/sql.go | 205 +++++++++++ driver/sql/sql_test.go | 671 +++++++++++++++++++++++++++++++++++++ go.mod | 50 +++ go.sum | 172 ++++++++++ norm.go | 77 +++++ 8 files changed, 1232 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 driver/sql/sql.go create mode 100644 driver/sql/sql_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 norm.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e54b998 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +on: [push, pull_request] +name: Test with Docker +jobs: + test: + runs-on: ubuntu-latest + services: + dind: + image: docker:23.0-rc-dind-rootless + ports: + - 2375:2375 + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: "1.19" + + - name: Test with Docker + run: go test -v ./... diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ed403d7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 WinPooh32 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e8ac8f --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# NORM + +![test](https://github.com/WinPooh32/norm/actions/workflows/test.yml/badge.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/WinPooh32/norm)](https://goreportcard.com/report/github.com/WinPooh32/norm) +[![Go Reference](https://pkg.go.dev/badge/github.com/WinPooh32/norm.svg)](https://pkg.go.dev/github.com/WinPooh32/norm) + +NORM is **N**ot an **ORM**. + +It does: + +- provide **unified** CRUD API for your queries; +- use generics for your types; +- **not** generate sql migrations; +- **not** generate queries from structs; +- **not** manage transactions. diff --git a/driver/sql/sql.go b/driver/sql/sql.go new file mode 100644 index 0000000..bff78c0 --- /dev/null +++ b/driver/sql/sql.go @@ -0,0 +1,205 @@ +package sql + +import ( + "context" + "database/sql" + "errors" + "fmt" + "reflect" + + "github.com/VauntDev/tqla" + "github.com/blockloop/scan/v2" + + "github.com/WinPooh32/norm" +) + +var tq, _ = tqla.New(tqla.WithPlaceHolder(tqla.Dollar)) + +func SetPlaceHolder(p tqla.Placeholder) { + tq, _ = tqla.New(tqla.WithPlaceHolder(p)) +} + +type txKey struct{} + +func WithTransaction(ctx context.Context, tx *sql.Tx) context.Context { + return context.WithValue(ctx, txKey{}, tx) +} + +func txValue(ctx context.Context) *sql.Tx { + tx := ctx.Value(txKey{}) + if tx == nil { + return nil + } + return tx.(*sql.Tx) +} + +func NewObject[Model, Args any](db *sql.DB, c, r, u, d string) norm.Object[Model, Args] { + var m Model + + return norm.NewObject[Model, Args]( + &pgCreator[Model, Args]{ + pgWriter[Model, Args]{db, c}, + }, + &pgReader[Model, Args]{ + db, r, isSlice(m), + }, + &pgUpdater[Model, Args]{ + pgWriter[Model, Args]{db, u}, + }, + &pgDeleter[Model, Args]{ + pgWriter[Model, Args]{db, d}, + }, + ) +} + +func NewView[Model, Args any](db *sql.DB, r string) norm.View[Model, Args] { + var m Model + + reader := &pgReader[Model, Args]{ + db: db, + tpl: r, + scanSlice: isSlice(m), + } + + return norm.NewView[Model, Args](reader) +} + +type preparer interface { + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) +} + +type a[Args any] struct { + A Args +} + +type ma[Model, Args any] struct { + M Model + A Args +} + +type pgCreator[Model, Args any] struct { + pgWriter[Model, Args] +} + +func (c *pgCreator[Model, Args]) Create(ctx context.Context, args Args, value Model) error { + return c.affect(ctx, args, value) +} + +type pgUpdater[Model, Args any] struct { + pgWriter[Model, Args] +} + +func (u *pgUpdater[Model, Args]) Update(ctx context.Context, args Args, value Model) error { + return u.affect(ctx, args, value) +} + +type pgDeleter[Model, Args any] struct { + pgWriter[Model, Args] +} + +func (d *pgDeleter[Model, Args]) Delete(ctx context.Context, args Args) error { + var nop Model + return d.affect(ctx, args, nop) +} + +type pgWriter[Model, Args any] struct { + db *sql.DB + tpl string +} + +func (w *pgWriter[Model, Args]) affect(ctx context.Context, args Args, value Model) error { + pr := newPreparer(txValue(ctx), w.db) + if err := w.exec(ctx, pr, args, value); err != nil { + return err + } + + return nil +} + +func (w *pgWriter[Model, Args]) exec(ctx context.Context, pr preparer, args Args, value Model) error { + stmtRaw, stmtArgs, err := tq.Compile(w.tpl, ma[Model, Args]{M: value, A: args}) + if err != nil { + return fmt.Errorf("compile query template: %w", err) + } + + stmt, err := pr.PrepareContext(ctx, stmtRaw) + if err != nil { + return fmt.Errorf("prepare query: %w", err) + } + + res, err := stmt.ExecContext(ctx, stmtArgs...) + if err != nil { + return err + } + + n, err := res.RowsAffected() + if err == nil && n <= 0 { + return norm.ErrNotAffected + } + + return nil +} + +type pgReader[Model, Args any] struct { + db *sql.DB + tpl string + scanSlice bool +} + +func (r *pgReader[Model, Args]) Read(ctx context.Context, args Args) (value Model, err error) { + pr := newPreparer(txValue(ctx), r.db) + + rows, err := r.query(ctx, pr, args) + if err != nil { + return value, err + } + + if r.scanSlice { + err = scan.RowsStrict(&value, rows) + if err != nil { + return value, fmt.Errorf("scan rows: %w", err) + } + } else { + err = scan.RowStrict(&value, rows) + if errors.Is(err, sql.ErrNoRows) { + return value, norm.ErrNotFound + } + if err != nil { + return value, fmt.Errorf("scan one row: %w", err) + } + } + + return value, nil +} + +func (r *pgReader[Model, Args]) query(ctx context.Context, pr preparer, args Args) (rows *sql.Rows, err error) { + stmtRaw, stmtArgs, err := tq.Compile(r.tpl, a[Args]{A: args}) + if err != nil { + return nil, fmt.Errorf("compile query template: %w", err) + } + + stmt, err := pr.PrepareContext(ctx, stmtRaw) + if err != nil { + return nil, fmt.Errorf("prepare query: %w", err) + } + + rows, err = stmt.QueryContext(ctx, stmtArgs...) + if err != nil { + return nil, fmt.Errorf("run query: %w", err) + } + + return rows, nil +} + +func isSlice(v any) bool { + return reflect.TypeOf(v).Kind() == reflect.Slice +} + +func newPreparer(tx *sql.Tx, db *sql.DB) (pr preparer) { + if tx != nil { + pr = tx + } else { + pr = db + } + return pr +} diff --git a/driver/sql/sql_test.go b/driver/sql/sql_test.go new file mode 100644 index 0000000..2adbb2c --- /dev/null +++ b/driver/sql/sql_test.go @@ -0,0 +1,671 @@ +package sql + +import ( + "context" + "database/sql" + "fmt" + "log" + "os" + "testing" + "time" + + "github.com/WinPooh32/norm" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/assert" + + "github.com/lib/pq" +) + +var db *sql.DB + +func TestMain(m *testing.M) { + // uses a sensible default on windows (tcp/http) and linux/osx (socket) + pool, err := dockertest.NewPool("") + if err != nil { + log.Fatalf("Could not construct pool: %s", err) + } + + err = pool.Client.Ping() + if err != nil { + log.Fatalf("Could not connect to Docker: %s", err) + } + + // pulls an image, creates a container based on it and runs it + resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "postgres", + Tag: "10", + Env: []string{ + "POSTGRES_PASSWORD=secret", + "POSTGRES_USER=user_name", + "POSTGRES_DB=dbname", + "listen_addresses = '*'", + }, + }, func(config *docker.HostConfig) { + // set AutoRemove to true so that stopped container goes away by itself + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + if err != nil { + log.Fatalf("Could not start resource: %s", err) + } + + hostAndPort := resource.GetHostPort("5432/tcp") + databaseUrl := fmt.Sprintf("postgres://user_name:secret@%s/dbname?sslmode=disable", hostAndPort) + + log.Println("Connecting to database on url: ", databaseUrl) + + resource.Expire(120) // Tell docker to hard kill the container in 120 seconds + + // exponential backoff-retry, because the application in the container might not be ready to accept connections yet + pool.MaxWait = 120 * time.Second + if err = pool.Retry(func() error { + db, err = sql.Open("postgres", databaseUrl) + if err != nil { + return err + } + return db.Ping() + }); err != nil { + log.Fatalf("Could not connect to docker: %s", err) + } + + //Run tests + code := m.Run() + + // You can't defer this because os.Exit doesn't care for defer + if err := pool.Purge(resource); err != nil { + log.Fatalf("Could not purge resource: %s", err) + } + + os.Exit(code) +} + +func resetDB(t testing.TB, db *sql.DB) error { + t.Helper() + + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + qq := []string{ + `DROP TABLE IF EXISTS "tests";`, + `CREATE TABLE "tests" ( + "id" text PRIMARY KEY, + "field_a" text NOT NULL, + "field_b" text NOT NULL, + "field_c" int NOT NULL, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL + );`, + `INSERT INTO "tests" VALUES( + 'id01', + 'a', + 'b', + 1234, + timestamp '2001-09-28 23:00', + timestamp '2001-09-28 23:00' + );`, + `INSERT INTO "tests" VALUES( + 'id02', + 'aaaa', + 'bbbb', + 4321, + timestamp '2002-09-28 23:00', + timestamp '2002-09-28 23:00' + );`, + `DROP TABLE IF EXISTS "tests_2";`, + `CREATE TABLE "tests_2" ( + "id" text PRIMARY KEY, + "field_a" text NOT NULL, + "field_b" text NOT NULL, + "field_c" int NOT NULL, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL + );`, + `INSERT INTO "tests_2" VALUES( + 'id03', + 'aa00', + 'bb00', + 1000, + timestamp '2003-09-28 23:00', + timestamp '2003-09-28 23:00' + );`, + } + + for _, q := range qq { + _, err := tx.Exec(q) + if err != nil { + return err + } + } + + err = tx.Commit() + if err != nil { + return err + } + + return nil +} + +type Model struct { + ID string `db:"id"` + FieldA string `db:"field_a"` + FieldB string `db:"field_b"` + FieldC int `db:"field_c"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type ModelShort struct { + FieldA string `db:"field_a"` + FieldB string `db:"field_b"` + FieldC int `db:"field_c"` +} + +type Args struct { + ID string + CreatedAt time.Time + UpdatedAt time.Time +} + +type FilterID struct { + ID string +} + +func setupQueries() (_ *sql.DB, c string, r string, u string, d string) { + c = ` +INSERT INTO "tests" ( + "id", + "field_a", + "field_b", + "field_c", + "created_at", + "updated_at" +) VALUES ( + {{.A.ID}}, + {{.M.FieldA}}, + {{.M.FieldB}}, + {{.M.FieldC}}, + {{.A.CreatedAt}}, + {{.A.UpdatedAt}} +); +` + + r = ` +SELECT + "id", + "field_a", + "field_b", + "field_c", + "created_at", + "updated_at" +FROM + "tests" +WHERE + "id" = {{.A.ID}} +;` + + u = ` +UPDATE + "tests" +SET + "field_a" = {{.M.FieldA}}, + "field_b" = {{.M.FieldB}}, + "field_c" = {{.M.FieldC}}, + "updated_at" = {{.A.UpdatedAt}} +WHERE + "id" = {{.A.ID}} +; +` + + d = ` +DELETE +FROM + "tests" +WHERE + "id" = {{.A.ID}} +` + + return db, c, r, u, d +} + +func setupViewQueries() (_ *sql.DB, r string) { + return db, ` + SELECT + "id", + "field_a", + "field_b", + "field_c", + "created_at", + "updated_at" + FROM + "tests" + WHERE + "id" = {{ .A.ID }} + ;` +} + +type ModelEmpty struct{} + +func TestObject_Create(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + modelObject := NewObject[ModelShort, Args](setupQueries()) + + err := modelObject.Create(context.Background(), + Args{ + ID: "qwerty", + CreatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + }, + ModelShort{ + FieldA: "a", + FieldB: "b", + FieldC: 1, + }, + ) + + assert.NoError(t, err) +} + +func TestObject_Create_Error_NotAffected(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + modelObject := NewObject[ModelEmpty, FilterID]( + db, + `INSERT INTO "tests_2" ( + "id", + "field_a", + "field_b", + "field_c", + "created_at", + "updated_at" + ) + ( + SELECT + "id", + "field_a", + "field_b", + "field_c", + "created_at", + "updated_at" + FROM + "tests_2" + WHERE + "id" = {{.A.ID}} + )`, + ``, ``, ``, + ) + + err := modelObject.Create(context.Background(), + FilterID{ + ID: "not found", + }, + ModelEmpty{}, + ) + + if assert.Error(t, err) { + assert.ErrorIs(t, err, norm.ErrNotAffected) + } +} + +func TestObject_Read(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + want := ModelShort{ + FieldA: "a", + FieldB: "b", + FieldC: 1, + } + + modelObject := NewObject[ModelShort, Args](setupQueries()) + + err := modelObject.Create(context.Background(), + Args{ + ID: "qwerty", + CreatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + }, + ModelShort{ + FieldA: "a", + FieldB: "b", + FieldC: 1, + }, + ) + if err != nil { + t.Fatal(err) + } + + modelObject = NewObject[ModelShort, Args](setupQueries()) + + got, err := modelObject.Read(context.Background(), Args{ID: "qwerty"}) + + assert.NoError(t, err) + assert.Equal(t, want, got) +} + +func TestObject_Read_Error_NotFound(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + modelObject := NewObject[ModelShort, Args](setupQueries()) + + err := modelObject.Create(context.Background(), + Args{ + ID: "qwerty", + CreatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + }, + ModelShort{ + FieldA: "a", + FieldB: "b", + FieldC: 1, + }, + ) + if err != nil { + t.Fatal(err) + } + + modelObject = NewObject[ModelShort, Args](setupQueries()) + + _, err = modelObject.Read(context.Background(), Args{ID: "qwerty123"}) + + if assert.Error(t, err) { + assert.ErrorIs(t, err, norm.ErrNotFound) + } +} + +func TestObject_Update(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + want := ModelShort{ + FieldA: "updated_a", + FieldB: "updated_b", + FieldC: 666, + } + + updateID := "id01" + + update := ModelShort{ + FieldA: "updated_a", + FieldB: "updated_b", + FieldC: 666, + } + + modelObject := NewObject[ModelShort, Args](setupQueries()) + + err := modelObject.Update(context.Background(), Args{ID: updateID}, update) + if err != nil { + t.Fatal(err) + } + + modelObject = NewObject[ModelShort, Args](setupQueries()) + + got, err := modelObject.Read(context.Background(), Args{ID: updateID}) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, want, got) +} + +func TestObject_Update_Error_NotAffected(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + updateID := "-1" + + modelObject := NewObject[ModelShort, Args](setupQueries()) + + err := modelObject.Update(context.Background(), Args{ID: updateID}, ModelShort{}) + + if assert.Error(t, err) { + assert.ErrorIs(t, err, norm.ErrNotAffected) + } +} + +func TestObject_Delete(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + deleteID := "id01" + + modelObject := NewObject[ModelShort, Args](setupQueries()) + + err := modelObject.Delete(context.Background(), Args{ID: deleteID}) + if err != nil { + t.Fatal(err) + } + + modelObject = NewObject[ModelShort, Args](setupQueries()) + + _, err = modelObject.Read(context.Background(), Args{ID: deleteID}) + + if assert.Error(t, err) { + assert.ErrorIs(t, err, norm.ErrNotFound) + } +} + +func TestObject_Delete_Error_NotAffected(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + deleteID := "-1" + + modelObject := NewObject[ModelShort, Args](setupQueries()) + + err := modelObject.Delete(context.Background(), Args{ID: deleteID}) + + if assert.Error(t, err) { + assert.ErrorIs(t, err, norm.ErrNotAffected) + } +} + +func TestObject_WithTransaction(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + tx1, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer tx1.Rollback() + + tx2, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer tx2.Rollback() + + ctxTx1 := WithTransaction(context.Background(), tx1) + ctxTx2 := WithTransaction(context.Background(), tx2) + + id := "id01" + + want := ModelShort{ + FieldA: "a", + FieldB: "b", + FieldC: 1234, + } + + modelObject := NewObject[ModelShort, Args](setupQueries()) + + err = modelObject.Delete(ctxTx1, Args{ID: id}) + if err != nil { + t.Fatal(err) + } + + _, errReadTx1 := modelObject.Read(ctxTx1, Args{ID: id}) + + got, err := modelObject.Read(ctxTx2, Args{ID: id}) + if err != nil { + t.Fatal(err) + } + + err = tx1.Commit() + if err != nil { + t.Fatal(err) + } + + err = tx2.Commit() + if err != nil { + t.Fatal(err) + } + + if assert.Error(t, errReadTx1) { + assert.ErrorIs(t, errReadTx1, norm.ErrNotFound) + } + + assert.Equal(t, want, got) +} + +func TestView_Read(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + want := Model{ + ID: "id01", + FieldA: "a", + FieldB: "b", + FieldC: 1234, + CreatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + } + + modelView := NewView[Model, FilterID](setupViewQueries()) + + got, err := modelView.Read(context.Background(), FilterID{ + ID: "id01", + }) + if err != nil { + t.Fatal(err) + } + + got.CreatedAt = got.CreatedAt.UTC() + got.UpdatedAt = got.UpdatedAt.UTC() + + assert.Equal(t, want, got) +} + +func TestView_Read_Error_NotFound(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + modelView := NewView[Model, FilterID](setupViewQueries()) + + _, err := modelView.Read(context.Background(), FilterID{ + ID: "-1", + }) + + if assert.Error(t, err) { + assert.ErrorIs(t, err, norm.ErrNotFound) + } +} + +type FilterIDs struct { + IDs pq.StringArray +} + +func TestView_Read_Slice(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + want := []Model{ + { + ID: "id01", + FieldA: "a", + FieldB: "b", + FieldC: 1234, + CreatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2001, 9, 28, 23, 0, 0, 0, time.UTC), + }, + { + ID: "id02", + FieldA: "aaaa", + FieldB: "bbbb", + FieldC: 4321, + CreatedAt: time.Date(2002, 9, 28, 23, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2002, 9, 28, 23, 0, 0, 0, time.UTC), + }, + } + + modelsView := NewView[[]Model, FilterIDs](db, ` + SELECT + "id", + "field_a", + "field_b", + "field_c", + "created_at", + "updated_at" + FROM + "tests" + WHERE + "id" = ANY( {{ .A.IDs }} ) + ORDER BY + "id" ASC + ;`, + ) + + got, err := modelsView.Read(context.Background(), FilterIDs{ + IDs: []string{"-1", "id01", "id02"}, + }) + if err != nil { + t.Fatal(err) + } + + for i := range got { + m := &got[i] + m.CreatedAt = m.CreatedAt.UTC() + m.UpdatedAt = m.UpdatedAt.UTC() + } + + assert.Equal(t, want, got) +} + +func TestView_Read_Slice_EmptyResult(t *testing.T) { + if err := resetDB(t, db); err != nil { + t.Fatal(err) + } + + modelsView := NewView[[]Model, FilterIDs](db, ` + SELECT + "id", + "field_a", + "field_b", + "field_c", + "created_at", + "updated_at" + FROM + "tests" + WHERE + "id" = ANY( {{ .A.IDs }} ) + ORDER BY + "id" ASC + ;`, + ) + + got, err := modelsView.Read(context.Background(), FilterIDs{ + IDs: []string{"-1", "-2", "-3"}, + }) + + assert.Len(t, got, 0) + assert.NoError(t, err) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e28c04b --- /dev/null +++ b/go.mod @@ -0,0 +1,50 @@ +module github.com/WinPooh32/norm + +go 1.19 + +require ( + github.com/VauntDev/tqla v0.0.1 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/blockloop/scan/v2 v2.5.0 + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/docker/cli v20.10.17+incompatible // indirect + github.com/docker/docker v20.10.7+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/lib/pq v1.10.9 + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.5 // indirect + github.com/ory/dockertest/v3 v3.10.0 + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.12.0 // indirect + golang.org/x/tools v0.7.0 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d2c148c --- /dev/null +++ b/go.sum @@ -0,0 +1,172 @@ +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/VauntDev/tqla v0.0.1 h1:NVoNgY+qIRzG2j+Kw6DyLfE274lvQBV9zV8e1BaJXrM= +github.com/VauntDev/tqla v0.0.1/go.mod h1:cwJGFN9JyZ/4kROc3jyR3TgW4OulSACJDH1qinWcuu8= +github.com/blockloop/scan/v2 v2.5.0 h1:/yNcCwftYn3wf5BJsJFO9E9P48l45wThdUnM3WcDF+o= +github.com/blockloop/scan/v2 v2.5.0/go.mod h1:OFYyMocUdRW3DUWehPI/fSsnpNMUNiyUaYXRMY5NMIY= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= +github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/proullon/ramsql v0.0.1 h1:tI7qN48Oj1LTmgdo4aWlvI9z45a4QlWaXlmdJ+IIfbU= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= diff --git a/norm.go b/norm.go new file mode 100644 index 0000000..f90f9f2 --- /dev/null +++ b/norm.go @@ -0,0 +1,77 @@ +package norm + +import ( + "context" + "errors" +) + +var ( + ErrNotFound = errors.New("not found") + ErrNotAffected = errors.New("not affected by create/update") +) + +type creator[Model, Args any] interface { + Create(ctx context.Context, key Args, value Model) error +} + +type reader[Model, Args any] interface { + Read(ctx context.Context, args Args) (value Model, err error) +} + +type updater[Model, Args any] interface { + Update(ctx context.Context, args Args, value Model) error +} + +type deleter[Model, Args any] interface { + Delete(ctx context.Context, args Args) error +} + +type Object[Model, Args any] struct { + creator[Model, Args] + reader[Model, Args] + updater[Model, Args] + deleter[Model, Args] +} + +func NewObject[Model, Args any]( + c creator[Model, Args], + r reader[Model, Args], + u updater[Model, Args], + d deleter[Model, Args], +) Object[Model, Args] { + return Object[Model, Args]{c, r, u, d} +} + +type PersistentObject[Model, Args any] struct { + creator[Model, Args] + reader[Model, Args] + updater[Model, Args] +} + +func NewPersistentObject[Model, Args any]( + c creator[Model, Args], + r reader[Model, Args], + u updater[Model, Args], +) PersistentObject[Model, Args] { + return PersistentObject[Model, Args]{c, r, u} +} + +type ImmutableObject[Model, Args any] struct { + creator[Model, Args] + reader[Model, Args] +} + +func NewImmutableObject[Model, Args any]( + c creator[Model, Args], + r reader[Model, Args], +) ImmutableObject[Model, Args] { + return ImmutableObject[Model, Args]{c, r} +} + +type View[Model, Args any] struct { + reader[Model, Args] +} + +func NewView[Model, Args any](r reader[Model, Args]) View[Model, Args] { + return View[Model, Args]{r} +}