diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..98358d8 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,5 +1,11 @@ + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9dbf198 --- /dev/null +++ b/README.md @@ -0,0 +1,365 @@ +# diamond + +### a golang ORM for mongodb that rocks 🎸~♬ + +# usage + +## installation + +run the following command in your terminal... + +```shell +go get rockfic.com/orm +``` + +...and import the package at the top of your file(s) like so: + +```go +package tutorial + +import "rockfic.com/orm" +``` + +## Connect to the database + +```go +package tutorial + +import "rockfic.com/orm" + +func main() { + orm.Connect("mongodb://127.0.0.1", "your_database") +} +``` + +this will create a connection and store it in the `DB` global variable. This global variable is used internally to +interact with the underlying database. + +if you need to ~~why?~~, you may also access the mongoDB client directly via the `orm.Client` +variable. + +## Create a Model + +to create a new model, you need to define a struct like this: + +```go +package tutorial + +import "rockfic.com/orm" + +type User struct { + orm.Document `bson:",inline" coll:"collection"` + ID int64 `bson:"_id"` + Username string `bson:"username"` + Email string `bson:"email"` + Friends []User `bson:"friends"` +} +``` + +this on its own is useless. to actually do anything useful with it, you need to *register* this struct as a model: + +```go +package tutorial + +import "rockfic.com/orm" + +func main() { + /* ~ snip ~ */ + orm.ModelRegistry.Model(User{}) +} +``` + +you can also pass multiple arguments to `orm.Model`, so long as they embed the `Document` struct. + +you can access the newly created model like this: + +```go +package tutorial + +import "rockfic.com/orm" + +func main() { + /* ~ snip ~ */ + userModel := orm.ModelRegistry.Get("User") +} +``` + +## Documents + +any struct can be used as a document, so long as it embeds the `Document` struct. the `Document` struct is special, in +that it turns any structs which embed it into `IDocument` implementors. +`Document` should be embedded with a `bson:",inline" tag, otherwise you will end up with something like this in the +database: + +```bson +{ + "document": { + "createdAt": ISODate('2001-09-11T05:37:18.742Z'), + "updatedAt": ISODate('2001-09-11T05:37:18.742Z') + }, + _id: 1, + username: "foobar", + email: "test@testi.ng", + friends: [] +} +``` + +a `coll` or `collection` tag is also required, to assign the model to a mongodb collection. this tag is only valid on +the embedded `Document` field, and each document can only have one collection associated with it. +obviously. +i mean seriously, who'd want to store one thing in two places? + +the recommended way to create a new document instance is via the `orm.Create` method: + +```go +package tutorial + +import "rockfic.com/orm" + +func main() { + /* ~ snip ~ */ + user := orm.Create(User{}).(*User) +} +``` + +similarly, to create a slice of documents, call `orm.CreateSlice`: + +```go +package tutorial + +import "rockfic.com/orm" + +func main() { + /* ~ snip ~ */ + users := orm.CreateSlice[User](User{}) +} +``` + +lastly, let's implement the `HasID` interface on our document: + +```go +package tutorial + +import "rockfic.com/orm" + +func (u *User) GetId() any { + return u.ID +} + +func (u *User) SetId(id any) { + u.ID = id.(int64) +} +``` + +#### but why do i need to implement this? :( + +other ORMs rudely assume that you want your `_id`s to be mongodb ObjectIDs, which are **fucking ugly**, but also, more +importantly, *may not match your existing schema.* +by doing things this way, you can set your _id to be any of the following types: + +- int +- int32 +- int64 +- uint +- uint32 +- uint64 +- string +- ObjectId + + (if you hate yourself that much) + +## finding/querying + +`Find`, `FindOne`, `FindByID`, and `FindPaged` all return an `*orm.Query` object. +to get the underlying value, simply call `.Exec()`, passing to it a pointer to the variable in which you wish to store +the results. + +### `Find` example + +```go +package tutorial + +import ( + "go.mongodb.org/mongo-driver/bson" + "rockfic.com/orm" +) + +func main() { + /* ~ snip ~ */ + userModel := orm.ModelRegistry.Get("User") + if userModel != nil { + res := orm.CreateSlice(User{}) + jq, _ := userModel.Find(bson.M{"username": "foobar"}) + q.Exec(&res) + } +} + +``` + +## saving/updating + +to save a document, simply call the `Save()` method. if the document doesn't exist, the ORM will create it, otherwise it +replaces the existing one. + +## referencing other documents + +it's possible to store references to other documents/structs, or slices to other documents. given the `User` struct we +defined above, which contains a slice (`Friends`) referencing other `User`s, we could change the `Friends` field in the +`User` type to be populateable, like so: + +```diff +type User struct { + orm.Document `bson:",inline" coll:"collection"` + ID int64 `bson:"_id"` + Username string `bson:"username"` + Email string `bson:"email"` +- Friends []User `bson:"friends"` ++ Friends []User `bson:"friends" ref:"User"` +} +``` + +assuming we've filled a user's `Friends` with a few friends... + +```go +package tutorial + +import "rockfic.com/orm" + +func main() { + /* ~ snip ~ */ + user := orm.Create(User{}).(*User) + for i := 0; i < 10; i++ { + friend := orm.Create(User{ID: int64(i + 2)}).(*User) + user.Friends = append(user.Friends, friend) + } +} + +``` + +...in the database, `friends` will be stored like this: + +```json5 +{ + // ~ snip ~ // + friends: [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] +} +``` + +after retrieving a `User` from the database, you can load their `Friends` and their info by using the `Populate`function +on the returned `Query` pointer, like in the following example: + +```go +package tutorial + +import ( + "fmt" + "go.mongodb.org/mongo-driver/bson" + "rockfic.com/orm" +) + +func main() { + /* ~ snip ~ */ + query, err := userModel.FindOne(bson.M{"_id": 1}) + if err == nil { + result := orm.Create(User{}).(*User) + query.Populate("Friends").Exec(result) + fmt.Printf("%+v", result) + } +} + +``` + +### Models vs Documents: what's the difference? + +a **`Model`** is a static type that contains information about a given Document-like type. a **`Document`**, on the +other hand, is a full-fledged instances with its own set of methods. +if you come from an object-oriented programming background, you can envision the relationship between a `Model` and +`Document` something like this (using Java as an example): + +```java +public class Document { + public void Pull(field String, elements ...Document) { + // ... + } + public void Append(field String, elements ...Document) { + // ... + } + public void Save() { + //... + } + public void Delete() { + //... + } + + // model methods // + public static Document FindOne(HashMap query, MongoOptions options) { + // ... + } + public static ArrayList FindAll(HashMap query, MongoOptions options) { + // ... + } +} +``` + +## Struct Tags + +the following tags are recognized by the ORM: + +### `ref` + +the syntax of the `ref` tag is: + +``` +`ref:"struct_name"` +``` + +where `struct_name` is the name of the struct you're referencing, as shown +in [Referencing other documents](#referencing-other-documents). + +### `gridfs` + +you can load files stored in [gridfs](https://www.mongodb.com/docs/manual/core/gridfs/) directly into your structs using +this tag, which has the following syntax: + +``` +`gridfs:"bucket_name,file_fmt" +``` + +where: + +- `bucket_name` is the name of the gridfs bucket +- `file_fmt` is a valid [go template string](https://pkg.go.dev/text/template) that resolves to the unique file name. + all exported methods and fields in the surrounding struct can be referenced in this template. + +currently, the supported field types for the `gridfs` tag are: + +- `string` +- `[]byte` + +### `idx`/`index` + +indexes can be defined using this tag on the field you want to create the index for. the syntax is as follows: + +`idx:{field || field1,field2,...},keyword1,keyword2,...` + +supported keywords are `unique` and `sparse`. +`background` is technically allowed, but it's deprecated, and so will essentially be a no-op on mongodb 4.2+. + +## acknowledgements + +- [goonode/mogo](https://github.com/goonode/mogo), the project which largely inspired this one + +# Further reading + +see the [godocs]("https://git.tablet.sh/tablet/diamond-orm/src/branch/main/godoc.html") for more details :) \ No newline at end of file diff --git a/godoc.html b/godoc.html new file mode 100644 index 0000000..7888386 --- /dev/null +++ b/godoc.html @@ -0,0 +1,1229 @@ + + + + + +orm - Go Documentation Server + + + + + + + + + +
+ ... +
+
+
+ +
+ +
+ +
+
+
+
+
+

+ Package orm + +

+ + + +
+
+
import "rockfic.com/orm"
+
+
+
Overview
+
Index
+ + +
Subdirectories
+ +
+
+ +
+ +
+

Overview â–¾

+ + +
+
+
+ +
+

Index â–¾

+ +
+
+ +
Constants
+ + +
Variables
+ + +
func Connect(uri string, dbName string)
+ + +
func Create(d any) any
+ + +
func CreateSlice[T any](d T) []*T
+ + +
type Counter
+ + +
type Document
+ + +
    func (d *Document) Append(field string, a + ...interface{}) error
+ + +
    func (d *Document) Delete() error
+ + +
    func (d *Document) Pull(field string, a ...any) + error
+ + +
    func (d *Document) Remove() error
+ + +
    func (d *Document) Save() error
+ + +
    func (d *Document) Swap(field string, i, j int) + error
+ + +
type DocumentSlice
+ + +
    func (d *DocumentSlice) Delete() error
+ + +
    func (d *DocumentSlice) Remove() error
+ + +
    func (d *DocumentSlice) Save() error
+ + +
type GridFSFile
+ + +
type HasID
+ + +
type HasIDSlice
+ + +
type IDocument
+ + +
type IDocumentSlice
+ + +
type IModel
+ + +
type InternalIndex
+ + +
type Model
+ + +
    func (m *Model) Find(query interface{}, opts + ...*options.FindOptions) (*Query, error)
+ + +
    func (m *Model) FindByID(id interface{}) (*Query, + error)
+ + +
    func (m *Model) FindOne(query interface{}, options + ...*options.FindOneOptions) (*Query, error)
+ + +
    func (m *Model) FindPaged(query interface{}, page + int64, perPage int64, opts ...*options.FindOptions) (*Query, error)
+ + +
    func (m *Model) FindRaw(query interface{}, opts + ...*options.FindOptions) (*mongo.Cursor, error)
+ + +
type Query
+ + +
    func (q *Query) Exec(result interface{})
+ + +
    func (q *Query) JSON() (string, error)
+ + +
    func (q *Query) LoadFile(fields ...string) *Query +
+ + +
    func (q *Query) Populate(fields ...string) *Query +
+ + +
type Reference
+ + +
type TModelRegistry
+ + +
    func (r TModelRegistry) Get(name string) + *Model
+ + +
    func (r TModelRegistry) Has(i interface{}) + (string, *Model, bool)
+ + +
    func (r TModelRegistry) HasByName(n string) + (string, *Model, bool)
+ + +
    func (r TModelRegistry) Index(n string) int +
+ + +
    func (r TModelRegistry) Model(mdl ...any) +
+ + +
+
+ + +

Package files

+

+ + + document.go + + document_internals.go + + document_slice.go + + errors.go + + gridfs.go + + idcounter.go + + indexes.go + + model.go + + query.go + + registry.go + + testing.go + + util.go + + +

+ +
+
+ + +

Constants

+ + +
const (
+    OP_FIND_ONE   = "findOne"
+    OP_FIND_PAGED = "findPaged"
+    OP_FIND_ALL   = "findAll"
+    OP_FIND       = "find"
+)
+ + +
const COUNTER_COL = "@counters"
+ + +

Variables

+ + +
var (
+    ErrNotASlice               = errors.New("Current object or field is not a slice!")
+    ErrNotAStruct              = errors.New("Current object or field is not a struct!")
+    ErrOutOfBounds             = errors.New("Index(es) out of bounds!")
+    ErrAppendMultipleDocuments = errors.New("Cannot append to multiple documents!")
+    ErrNotSliceOrStruct        = errors.New("Current object or field is not a slice nor a struct!")
+    ErrUnsupportedID           = errors.New("Unknown or unsupported id type provided")
+)
+ +

DB - The mongodb database handle +

var DB *mongo.Database
+ +

DBClient - The mongodb client +

var DBClient *mongo.Client
+ +

ModelRegistry - the ModelRegistry stores a map containing + pointers to Model instances, keyed by an associated + model name +

var ModelRegistry = make(TModelRegistry)
+ +

NextStringID - Override this function with your own + string ID generator! +

var NextStringID func() string
+ + +

func Connect + + + +

+
func Connect(uri string, dbName string)
+ + +

func Create + + + +

+
func Create(d any) any
+

Create creates a new instance of a given Document + type and returns a pointer to it. + + +

func CreateSlice + + + +

+
func CreateSlice[T any](d T) []*T
+

CreateSlice - convenience method which creates a new slice + of type *T (where T is a type which embeds Document) and + returns it + + +

type Counter + + + +

+ +
type Counter struct {
+    Current    any    `bson:"current"`
+    Collection string `bson:"collection"`
+}
+
+ + +

type Document + + + +

+ +
type Document struct {
+    // Created time. updated/added automatically.
+    Created time.Time `bson:"createdAt" json:"createdAt"`
+    // Modified time. updated/added automatically.
+    Modified time.Time `bson:"updatedAt" json:"updatedAt"`
+    // contains filtered or unexported fields
+}
+
+ + +

func (*Document) Append + + + +

+
func (d *Document) Append(field string, a ...interface{}) error
+

Append appends one or more items to `field`. + will error if this Model contains a reference + to multiple documents, or if `field` is not a + slice. + + +

func (*Document) Delete + + + +

+
func (d *Document) Delete() error
+

Delete - deletes a model instance from the database + + +

func (*Document) Pull + + + +

+
func (d *Document) Pull(field string, a ...any) error
+

Pull - removes elements from the subdocument slice stored in `field`. + + +

func (*Document) Remove + + + +

+
func (d *Document) Remove() error
+

Remove - alias for Delete + + +

func (*Document) Save + + + +

+
func (d *Document) Save() error
+

Save - updates this Model in the database, + or inserts it if it doesn't exist + + +

func (*Document) Swap + + + +

+
func (d *Document) Swap(field string, i, j int) error
+

Swap - swaps the elements at indexes `i` and `j` in the + slice stored at `field` + + +

type DocumentSlice + + + +

+ +
type DocumentSlice []IDocument
+ + +

func (*DocumentSlice) Delete + + + +

+
func (d *DocumentSlice) Delete() error
+ + +

func (*DocumentSlice) Remove + + + +

+
func (d *DocumentSlice) Remove() error
+ + +

func (*DocumentSlice) Save + + + +

+
func (d *DocumentSlice) Save() error
+ + +

type GridFSFile + + + +

+ +
type GridFSFile struct {
+    ID     primitive.ObjectID `bson:"_id"`
+    Name   string             `bson:"filename"`
+    Length int                `bson:"length"`
+}
+
+ + +

type HasID + + + +

+

HasID is a simple interface that you must implement + in your models, using a pointer receiver. + This allows for more flexibility in cases where + your ID isn't an ObjectID (e.g., int, uint, string...). +

and yes, those darn ugly ObjectIDs are supported :) +

type HasID interface {
+    Id() any
+    SetId(id any)
+}
+ + +

type HasIDSlice + + + +

+ +
type HasIDSlice []HasID
+ + +

type IDocument + + + +

+ +
type IDocument interface {
+    Append(field string, a ...interface{}) error
+    Pull(field string, a ...any) error
+    Swap(field string, i, j int) error
+    Delete() error
+    Remove() error
+    Save() error
+    // contains filtered or unexported methods
+}
+ + +

type IDocumentSlice + + + +

+ +
type IDocumentSlice interface {
+    Delete() error
+    Remove() error
+    Save() error
+    // contains filtered or unexported methods
+}
+ + +

type IModel + + + +

+ +
type IModel interface {
+    FindRaw(query interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error)
+    Find(query interface{}, opts ...*options.FindOptions) (*Query, error)
+    FindByID(id interface{}) (*Query, error)
+    FindOne(query interface{}, options ...*options.FindOneOptions) (*Query, error)
+    FindPaged(query interface{}, page int64, perPage int64, options ...*options.FindOptions) (*Query, error)
+    // contains filtered or unexported methods
+}
+ + +

type InternalIndex + + + +

+

prolly won't need to use indexes, but just in case... +

type InternalIndex struct {
+    Fields  []string
+    Options []string
+    // contains filtered or unexported fields
+}
+
+ + +

type Model + + + +

+

Model - type which contains "static" methods like + Find, FindOne, etc. +

type Model struct {
+    Indexes map[string][]InternalIndex
+    Type    reflect.Type
+    // contains filtered or unexported fields
+}
+
+ + +

func (*Model) Find + + + +

+
func (m *Model) Find(query interface{}, opts ...*options.FindOptions) (*Query, error)
+

Find - find all documents satisfying `query`. + returns a pointer to a Query for further chaining. + + +

func (*Model) FindByID + + + +

+
func (m *Model) FindByID(id interface{}) (*Query, error)
+

FindByID - find a single document by its _id field. + Wrapper around FindOne with an ID query as its first argument + + +

func (*Model) FindOne + + + +

+
func (m *Model) FindOne(query interface{}, options ...*options.FindOneOptions) (*Query, error)
+

FindOne - find a single document satisfying `query`. + returns a pointer to a Query for further chaining. + + +

func (*Model) FindPaged + + + +

+
func (m *Model) FindPaged(query interface{}, page int64, perPage int64, opts ...*options.FindOptions) (*Query, error)
+

FindPaged - Wrapper around FindAll with the Skip and Limit options populated. + returns a pointer to a Query for further chaining. + + +

func (*Model) FindRaw + + + +

+
func (m *Model) FindRaw(query interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error)
+

FindRaw - find documents satisfying `query` and return a plain mongo cursor. + + +

type Query + + + +

+ +
type Query struct {
+    // contains filtered or unexported fields
+}
+
+ + +

func (*Query) Exec + + + +

+
func (q *Query) Exec(result interface{})
+

Exec - executes the query and puts its results into the + provided argument. +

Will panic if called more than once on the same Query instance. + + +

func (*Query) JSON + + + +

+
func (q *Query) JSON() (string, error)
+

JSON - marshals this Query's results into json format + + +

func (*Query) LoadFile + + + +

+
func (q *Query) LoadFile(fields ...string) *Query
+

LoadFile - loads the contents of one or more files + stored in gridFS into the fields named by `fields`. +

gridFS fields can be either a `string` or `[]byte`, and are + tagged with `gridfs:"BUCKET,FILE_FORMAT` + where: +

    +
  • `BUCKET` is the name of the bucket where the files are stored +
  • `FILE_FORMAT` is any valid go template string that resolves to + the unique file name. + all exported values and methods present in the surrounding + struct can be used in this template. +
+ + +

func (*Query) Populate + + + +

+
func (q *Query) Populate(fields ...string) *Query
+

Populate populates document references via reflection + + +

type Reference + + + +

+

Reference stores a typed document reference +

type Reference struct {
+    // owning model name
+    Model string
+    // the name of the struct field
+    FieldName string
+    // index of field in owning struct
+    Idx int
+    // the type of the referenced object
+    HydratedType reflect.Type
+
+    // field kind (struct, slice, ...)
+    Kind reflect.Kind
+    // contains filtered or unexported fields
+}
+
+ + +

type TModelRegistry + + + +

+ +
type TModelRegistry map[string]*Model
+ + +

func (TModelRegistry) Get + + + +

+
func (r TModelRegistry) Get(name string) *Model
+ + +

func (TModelRegistry) Has + + + +

+
func (r TModelRegistry) Has(i interface{}) (string, *Model, bool)
+

Has returns the model typename and Model instance corresponding + to the argument passed, as well as a boolean indicating whether it + was found. otherwise returns `"", nil, false` + + +

func (TModelRegistry) HasByName + + + +

+
func (r TModelRegistry) HasByName(n string) (string, *Model, bool)
+

HasByName functions almost identically to Has, + except that it takes a string as its argument. + + +

func (TModelRegistry) Index + + + +

+
func (r TModelRegistry) Index(n string) int
+

Index returns the index at which the Document struct is embedded + + +

func (TModelRegistry) Model + + + +

+
func (r TModelRegistry) Model(mdl ...any)
+

Model registers models in the ModelRegistry, where + they can be accessed via a model's struct name + + +

Subdirectories

+ +
+ + + + + + + + + + + + + + + + + + + +
NameSynopsis
..
+ muck + + You can edit this code! Click here and start typing. +
+
+ +
+
diff --git a/model.go b/model.go index 050f299..f98877b 100644 --- a/model.go +++ b/model.go @@ -10,7 +10,8 @@ import ( "unsafe" ) -// Model - "base" struct for all queryable models +// Model - type which contains "static" methods like +// Find, FindOne, etc. type Model struct { Indexes map[string][]InternalIndex Type reflect.Type @@ -224,6 +225,9 @@ func Create(d any) any { return what } +// CreateSlice - convenience method which creates a new slice +// of type *T (where T is a type which embeds Document) and +// returns it func CreateSlice[T any](d T) []*T { r, _, _ := createBase(d) rtype := r.Type() @@ -232,7 +236,3 @@ func CreateSlice[T any](d T) []*T { newItem.Elem().Set(reflect.MakeSlice(rslice, 0, 0)) return newItem.Elem().Interface().([]*T) } - -func (m *Model) PrintMe() { - fmt.Printf("My name is %s !\n", nameOf(m)) -}