Compare commits
No commits in common. "main" and "backup/main" have entirely different histories.
main
...
backup/mai
@ -1,3 +0,0 @@
|
||||
((nil . ((elcord-project-name . "the diamond ORM")
|
||||
(elcord-project-description . "a mongodb ORM for golang that rocks!")
|
||||
)))
|
16
.idea/.gitignore
generated
vendored
16
.idea/.gitignore
generated
vendored
@ -1,8 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
49
.idea/inspectionProfiles/Project_Default.xml
generated
49
.idea/inspectionProfiles/Project_Default.xml
generated
@ -1,49 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="CssDeprecatedValue" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidAtRule" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidCharsetRule" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidCustomPropertyAtRuleDeclaration" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidCustomPropertyAtRuleName" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidFunction" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidHtmlTagReference" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidImport" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidMediaFeature" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidPropertyValue" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssInvalidPseudoSelector" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssMissingComma" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="CssNegativeValue" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssNoGenericFontName" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="CssNonIntegerLengthInPixels" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="CssOverwrittenProperties" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="CssRedundantUnit" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="CssReplaceWithShorthandSafely" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="CssReplaceWithShorthandUnsafely" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||
<inspection_tool class="CssUnknownProperty" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="myCustomPropertiesEnabled" value="false" />
|
||||
<option name="myIgnoreVendorSpecificProperties" value="false" />
|
||||
<option name="myCustomPropertiesList">
|
||||
<value>
|
||||
<list size="0" />
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="CssUnknownTarget" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssUnknownUnit" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssUnresolvedClassInComposesRule" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssUnresolvedCustomProperty" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="CssUnusedSymbol" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="GoErrorStringFormat" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="GoMixedReceiverTypes" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="GoSwitchMissingCasesForIotaConsts" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
|
||||
<inspection_tool class="GoUnhandledErrorResult" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
|
||||
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
379
README.md
Normal file
379
README.md
Normal file
@ -0,0 +1,379 @@
|
||||
# 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"
|
||||
```
|
||||
|
||||
## Convert your database to a replica set (if you haven't already)
|
||||
|
||||
this is **very important!!** otherwise, functionality like
|
||||
auto-incrementing IDs will not be available.
|
||||
|
||||
add these lines to `mongod.conf`:
|
||||
|
||||
```
|
||||
replication:
|
||||
replSetName: "rs0"
|
||||
```
|
||||
|
||||
of course, you can replace `rs0` with whatever name you want.
|
||||
|
||||
## 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 <sub><sub>~~why?~~</sub></sub>, 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.
|
||||
<sub>obviously.</sub>
|
||||
<sub><sub>i mean seriously, who'd want to store one thing in two places?</sub></sub>
|
||||
|
||||
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
|
||||
|
||||
<sub>(if you hate yourself that much)</sub>
|
||||
|
||||
## 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<String, Object> query, MongoOptions options) {
|
||||
// ...
|
||||
}
|
||||
public static ArrayList<Document> FindAll(HashMap<String, Object> 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 :)
|
340
document.go
340
document.go
@ -1 +1,341 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Document struct {
|
||||
// Created time. updated/added automatically.
|
||||
Created time.Time `bson:"createdAt" json:"createdAt" tstype:"Date"`
|
||||
// Modified time. updated/added automatically.
|
||||
Modified time.Time `bson:"updatedAt" json:"updatedAt" tstype:"Date"`
|
||||
model *Model `bson:"-"`
|
||||
exists bool `bson:"-"`
|
||||
self any `bson:"-"`
|
||||
raw any `bson:"-"`
|
||||
populatedFields map[string]bool `bson:"-"`
|
||||
}
|
||||
|
||||
func (d *Document) UnmarshalJSON(bytes []byte) error {
|
||||
var fiv interface{}
|
||||
if err := json.Unmarshal(bytes, &fiv); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
switch fiv.(type) {
|
||||
case []interface{}:
|
||||
tmpV := make(bson.A, 0)
|
||||
err = json.Unmarshal(bytes, &tmpV)
|
||||
typ := reflect.SliceOf(d.model.Type)
|
||||
//d.SetSelf()
|
||||
var arr []interface{}
|
||||
reified := rerere(tmpV, typ, true)
|
||||
if ta, ok := reified.(bson.A); ok {
|
||||
arr = []interface{}(ta)
|
||||
} else {
|
||||
arr = reified.([]interface{})
|
||||
}
|
||||
fmt.Println(len(arr))
|
||||
break
|
||||
case map[string]interface{}:
|
||||
tmpV := make(bson.M)
|
||||
err = json.Unmarshal(bytes, &tmpV)
|
||||
typ := reflect.PointerTo(d.model.Type)
|
||||
self := reflect.ValueOf(d.self)
|
||||
nself := reflect.NewAt(typ.Elem(), self.UnsafePointer())
|
||||
reified := rerere(tmpV, typ, true)
|
||||
nself.Elem().Set(reflect.ValueOf(reified).Elem())
|
||||
d.self = nself.Interface()
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Document) MarshalJSON() ([]byte, error) {
|
||||
v := serializeIDs((d).self, true, d.populatedFields, "")
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
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
|
||||
SaveWith(opts *SaveOptions) error
|
||||
Populate(fields ...string)
|
||||
SetSelf(arg interface{})
|
||||
getExists() bool
|
||||
setExists(n bool)
|
||||
setModified(Modified time.Time)
|
||||
setCreated(Modified time.Time)
|
||||
getModified() time.Time
|
||||
getCreated() time.Time
|
||||
serializeToStore() any
|
||||
getModel() *Model
|
||||
setModel(m Model)
|
||||
markPopulated(field string)
|
||||
markDepopulated(field string)
|
||||
newPopulationMap()
|
||||
getPopulated() map[string]bool
|
||||
getRaw() any
|
||||
setRaw(raw any)
|
||||
}
|
||||
|
||||
type SaveOptions struct {
|
||||
SetTimestamps bool
|
||||
}
|
||||
|
||||
func (d *Document) getCreated() time.Time {
|
||||
return d.Created
|
||||
}
|
||||
|
||||
func (d *Document) setCreated(Created time.Time) {
|
||||
d.Created = Created
|
||||
}
|
||||
|
||||
func (d *Document) getModified() time.Time {
|
||||
return d.Modified
|
||||
}
|
||||
|
||||
func (d *Document) setModified(Modified time.Time) {
|
||||
d.Modified = Modified
|
||||
}
|
||||
|
||||
// SetSelf - don't call this lol
|
||||
func (d *Document) SetSelf(arg interface{}) {
|
||||
d.self = arg
|
||||
}
|
||||
|
||||
func (d *Document) getModel() *Model {
|
||||
return d.model
|
||||
}
|
||||
|
||||
func (d *Document) setModel(m Model) {
|
||||
d.model = &m
|
||||
}
|
||||
|
||||
func (d *Document) getExists() bool {
|
||||
return d.exists
|
||||
}
|
||||
func (d *Document) setExists(n bool) {
|
||||
d.exists = n
|
||||
}
|
||||
|
||||
// Delete - deletes a model instance from the database
|
||||
func (d *Document) Delete() error {
|
||||
var err error
|
||||
val := valueOf(d.self)
|
||||
if val.Kind() == reflect.Slice {
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
cur := val.Index(i)
|
||||
if err = doDelete(d, cur.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return doDelete(d, d.self)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove - alias for Delete
|
||||
func (d *Document) Remove() error {
|
||||
return d.Delete()
|
||||
}
|
||||
|
||||
// SaveWith - updates this Model in the database,
|
||||
// or inserts it if it doesn't exist, using the provided
|
||||
// SaveOptions
|
||||
func (d *Document) SaveWith(opts *SaveOptions) error {
|
||||
val := valueOf(d.self)
|
||||
if val.Kind() == reflect.Slice {
|
||||
for i := range val.Len() {
|
||||
cur := val.Index(i)
|
||||
if err := doSave(d.model.Collection(), !d.exists, opts, cur.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return doSave(d.model.Collection(), !d.exists, opts, d.self)
|
||||
}
|
||||
}
|
||||
|
||||
// Save - updates this Model in the database,
|
||||
// or inserts it if it doesn't exist, using
|
||||
// default SaveOptions
|
||||
func (d *Document) Save() error {
|
||||
return d.SaveWith(&SaveOptions{
|
||||
SetTimestamps: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Document) serializeToStore() any {
|
||||
return serializeIDs((d).self, false, d.populatedFields, "")
|
||||
}
|
||||
|
||||
// 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 (d *Document) Append(field string, a ...interface{}) error {
|
||||
var d0 IDocument = d
|
||||
d0.getCreated()
|
||||
rv := reflect.ValueOf(d.self)
|
||||
selfRef := rv
|
||||
rt := reflect.TypeOf(d.self)
|
||||
if selfRef.Kind() == reflect.Pointer {
|
||||
selfRef = selfRef.Elem()
|
||||
rt = rt.Elem()
|
||||
}
|
||||
if err := checkStruct(selfRef); err != nil {
|
||||
return err
|
||||
}
|
||||
_, origV, err := getNested(field, selfRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
origRef := makeSettable(*origV, (*origV).Interface())
|
||||
fv := origRef
|
||||
if fv.Kind() == reflect.Pointer {
|
||||
fv = fv.Elem()
|
||||
}
|
||||
if fv.Kind() != reflect.Slice {
|
||||
return ErrNotASlice
|
||||
}
|
||||
for _, b := range a {
|
||||
val := reflect.ValueOf(incrementTagged(b))
|
||||
fv.Set(reflect.Append(fv, val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pull - removes elements from the subdocument slice stored in `field`.
|
||||
func (d *Document) Pull(field string, a ...any) error {
|
||||
rv := reflect.ValueOf(d.self)
|
||||
selfRef := rv
|
||||
rt := reflect.TypeOf(d.self)
|
||||
if selfRef.Kind() == reflect.Pointer {
|
||||
selfRef = selfRef.Elem()
|
||||
rt = rt.Elem()
|
||||
}
|
||||
if err := checkStruct(selfRef); err != nil {
|
||||
return err
|
||||
}
|
||||
_, origV, err := getNested(field, selfRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
origRef := makeSettable(*origV, (*origV).Interface())
|
||||
fv := origRef
|
||||
if fv.Kind() == reflect.Pointer {
|
||||
fv = fv.Elem()
|
||||
}
|
||||
if fv.Kind() != reflect.Slice {
|
||||
return ErrNotASlice
|
||||
}
|
||||
for _, b := range a {
|
||||
inner:
|
||||
for i := 0; i < fv.Len(); i++ {
|
||||
if reflect.DeepEqual(b, fv.Index(i).Interface()) {
|
||||
fv.Set(pull(fv, i, fv.Index(i).Type()))
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Swap - swaps the elements at indexes `i` and `j` in the
|
||||
// slice stored at `field`
|
||||
func (d *Document) Swap(field string, i, j int) error {
|
||||
rv := reflect.ValueOf(d.self)
|
||||
selfRef := rv
|
||||
rt := reflect.TypeOf(d.self)
|
||||
if selfRef.Kind() == reflect.Pointer {
|
||||
selfRef = selfRef.Elem()
|
||||
rt = rt.Elem()
|
||||
}
|
||||
if err := checkStruct(selfRef); err != nil {
|
||||
return err
|
||||
}
|
||||
_, origV, err := getNested(field, selfRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
origRef := makeSettable(*origV, (*origV).Interface())
|
||||
fv := origRef
|
||||
if fv.Kind() == reflect.Pointer {
|
||||
fv = fv.Elem()
|
||||
}
|
||||
if err = checkSlice(fv); err != nil {
|
||||
return err
|
||||
}
|
||||
if i >= fv.Len() || j >= fv.Len() {
|
||||
return ErrOutOfBounds
|
||||
}
|
||||
oi := fv.Index(i).Interface()
|
||||
oj := fv.Index(j).Interface()
|
||||
|
||||
fv.Index(i).Set(reflect.ValueOf(oj))
|
||||
fv.Index(j).Set(reflect.ValueOf(oi))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Document) Populate(fields ...string) {
|
||||
_, cm, _ := ModelRegistry.HasByName(d.model.typeName)
|
||||
|
||||
if cm != nil {
|
||||
rawDoc := d.raw
|
||||
for _, field := range fields {
|
||||
// 0 = fieldname, 1 = typename, 2 = bson name
|
||||
|
||||
r, refOk := cm.references[field]
|
||||
|
||||
if refOk {
|
||||
// get self
|
||||
// get ptr
|
||||
// find
|
||||
// unmarshal...
|
||||
htt := r.HydratedType
|
||||
if htt.Kind() == reflect.Pointer || htt.Kind() == reflect.Slice {
|
||||
htt = htt.Elem()
|
||||
}
|
||||
if strings.HasSuffix(field, ".") || strings.HasPrefix(field, ".") {
|
||||
log.Printf("WARN: invalid field name passed to Populate(). skipping...\n")
|
||||
continue
|
||||
}
|
||||
|
||||
tto := r.HydratedType
|
||||
if tto.Kind() == reflect.Pointer || tto.Kind() == reflect.Slice {
|
||||
tto = tto.Elem()
|
||||
}
|
||||
_, refColl, _ := ModelRegistry.HasByName(tto.Name())
|
||||
var tmp1 interface{}
|
||||
asIDocument, docOk := d.self.(IDocument)
|
||||
if docOk {
|
||||
asIDocument.markPopulated(field)
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(d.self)
|
||||
tt := v.Elem().Type()
|
||||
|
||||
tmp1 = populate(r, d.populatedFields, refColl.collection, rawDoc, field, d.self)
|
||||
nv := reflect.NewAt(tt, v.UnsafePointer())
|
||||
nv.Elem().Set(reflect.ValueOf(tmp1).Elem())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
340
document_internals.go
Normal file
340
document_internals.go
Normal file
@ -0,0 +1,340 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fatih/structtag"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
func serializeIDs(input interface{}, isJson bool, populated map[string]bool, parent string) interface{} {
|
||||
var key string
|
||||
if isJson {
|
||||
key = "json"
|
||||
} else {
|
||||
key = "bson"
|
||||
}
|
||||
vp := reflect.ValueOf(input)
|
||||
mt := reflect.TypeOf(input)
|
||||
var ret interface{}
|
||||
if vp.Kind() != reflect.Ptr {
|
||||
if vp.CanAddr() {
|
||||
vp = vp.Addr()
|
||||
} else {
|
||||
vp = makeSettable(vp, input)
|
||||
}
|
||||
}
|
||||
|
||||
if mt.Kind() == reflect.Pointer {
|
||||
mt = mt.Elem()
|
||||
}
|
||||
getID := func(bbb interface{}) interface{} {
|
||||
mptr := reflect.ValueOf(bbb)
|
||||
if mptr.Kind() != reflect.Pointer {
|
||||
mptr = makeSettable(mptr, bbb)
|
||||
}
|
||||
ifc, ok := mptr.Interface().(HasID)
|
||||
if ok {
|
||||
return ifc.Id()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
switch vp.Elem().Kind() {
|
||||
case reflect.Struct:
|
||||
ret0 := bson.M{}
|
||||
for i := 0; i < vp.Elem().NumField(); i++ {
|
||||
fv := vp.Elem().Field(i)
|
||||
ft := mt.Field(i)
|
||||
var descent string
|
||||
if parent != "" {
|
||||
descent = parent + "." + ft.Name
|
||||
} else {
|
||||
descent = ft.Name
|
||||
}
|
||||
tag, err := structtag.Parse(string(ft.Tag))
|
||||
panik(err)
|
||||
bbson, err := tag.Get(key)
|
||||
if err != nil || bbson.Name == "-" {
|
||||
continue
|
||||
}
|
||||
if bbson.Name == "" {
|
||||
marsh, _ := bson.Marshal(fv.Interface())
|
||||
unmarsh := bson.M{}
|
||||
bson.Unmarshal(marsh, &unmarsh)
|
||||
for k, v := range unmarsh {
|
||||
ret0[k] = v
|
||||
}
|
||||
} else {
|
||||
_, terr := tag.Get("ref")
|
||||
if reflect.ValueOf(fv.Interface()).Type().Kind() != reflect.Pointer {
|
||||
vp1 := reflect.New(fv.Type())
|
||||
vp1.Elem().Set(reflect.ValueOf(fv.Interface()))
|
||||
fv.Set(vp1.Elem())
|
||||
}
|
||||
var ip bool
|
||||
for k1, v1 := range populated {
|
||||
if k1 == descent {
|
||||
ip = v1
|
||||
break
|
||||
}
|
||||
}
|
||||
if iidoc, ok := input.(IDocument); ok && !ip {
|
||||
for k1, v1 := range iidoc.getPopulated() {
|
||||
if k1 == descent || k1 == ft.Name {
|
||||
ip = v1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if terr == nil {
|
||||
ifc, ok := fv.Interface().(HasID)
|
||||
if fv.Kind() == reflect.Slice {
|
||||
rarr := bson.A{}
|
||||
for j := 0; j < fv.Len(); j++ {
|
||||
if !isJson {
|
||||
rarr = append(rarr, getID(fv.Index(j).Interface()))
|
||||
} else {
|
||||
if ip {
|
||||
rarr = append(rarr, serializeIDs(fv.Index(j).Interface(), isJson, populated, descent))
|
||||
} else {
|
||||
rarr = append(rarr, getID(fv.Index(j).Interface()))
|
||||
}
|
||||
}
|
||||
}
|
||||
ret0[bbson.Name] = rarr
|
||||
} else if !ok {
|
||||
panic(fmt.Sprintf("referenced model slice at '%s.%s' does not implement HasID", nameOf(input), ft.Name))
|
||||
} else {
|
||||
if reflect.ValueOf(ifc).IsNil() || reflect.ValueOf(ifc.Id()).IsZero() {
|
||||
ret0[bbson.Name] = nil
|
||||
} else {
|
||||
if !isJson {
|
||||
ret0[bbson.Name] = ifc.Id()
|
||||
} else {
|
||||
if ip && bbson.Name != "-" {
|
||||
ret0[bbson.Name] = serializeIDs(fv.Interface(), isJson, populated, descent)
|
||||
} else if bbson.Name != "-" {
|
||||
ret0[bbson.Name] = ifc.Id()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if fv.Type() == reflect.TypeFor[time.Time]() {
|
||||
ret0[bbson.Name] = fv.Interface()
|
||||
} else {
|
||||
ret0[bbson.Name] = serializeIDs(fv.Interface(), isJson, populated, descent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret = ret0
|
||||
}
|
||||
case reflect.Slice:
|
||||
ret0 := bson.A{}
|
||||
for i := 0; i < vp.Elem().Len(); i++ {
|
||||
|
||||
ret0 = append(ret0, serializeIDs(vp.Elem().Index(i).Addr().Interface(), isJson, populated, parent))
|
||||
}
|
||||
ret = ret0
|
||||
default:
|
||||
ret = vp.Elem().Interface()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
func doSave(c *mongo.Collection, isNew bool, opts *SaveOptions, arg interface{}) error {
|
||||
var err error
|
||||
d, ok := arg.(IDocument)
|
||||
if !ok {
|
||||
return fmt.Errorf(errFmtNotAModel, nameOf(arg))
|
||||
}
|
||||
d.SetSelf(d)
|
||||
now := time.Now()
|
||||
selfo := reflect.ValueOf(d)
|
||||
vp := selfo
|
||||
if vp.Kind() != reflect.Ptr {
|
||||
vp = reflect.New(selfo.Type())
|
||||
vp.Elem().Set(selfo)
|
||||
}
|
||||
var asHasId = vp.Interface().(HasID)
|
||||
var asModel = vp.Interface().(IDocument)
|
||||
if (isNew && reflect.ValueOf(asHasId.Id()).IsZero()) && opts.SetTimestamps {
|
||||
d.setCreated(now)
|
||||
}
|
||||
if opts.SetTimestamps {
|
||||
d.setModified(now)
|
||||
}
|
||||
idxs := d.getModel().getIdxs()
|
||||
for _, i := range idxs {
|
||||
_, err = c.Indexes().CreateOne(context.TODO(), *i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isNew {
|
||||
nid := getLastInColl(c.Name(), asHasId.Id())
|
||||
pnid := incrementInterface(nid)
|
||||
if reflect.ValueOf(asHasId.Id()).IsZero() {
|
||||
(asHasId).SetId(pnid)
|
||||
}
|
||||
incrementAll(asHasId)
|
||||
_, im, _ := ModelRegistry.HasByName(asModel.getModel().getTypeName())
|
||||
_ = gridFsSave(asHasId, *im)
|
||||
|
||||
_, err = c.InsertOne(context.TODO(), d.serializeToStore())
|
||||
if err == nil {
|
||||
d.setExists(true)
|
||||
} else {
|
||||
_, err = c.UpdateOne(context.TODO(), bson.D{{Key: "_id", Value: d.(HasID).Id()}}, bson.M{
|
||||
"$set": d.serializeToStore(),
|
||||
})
|
||||
//_, err = c.ReplaceOne(context.TO_DO(), bson.D{{Key: "_id", Value: d.(HasID).Id()}}, d.serializeToStore())
|
||||
}
|
||||
} else {
|
||||
//_, err = c.ReplaceOne(context.TO_DO(), bson.D{{Key: "_id", Value: d.(HasID).Id()}}, d.serializeToStore())
|
||||
_, im, _ := ModelRegistry.HasByName(asModel.getModel().getTypeName())
|
||||
_ = gridFsSave(asHasId, *im)
|
||||
|
||||
_, err = c.UpdateOne(context.TODO(), bson.D{{Key: "_id", Value: d.(HasID).Id()}}, bson.M{
|
||||
"$set": d.serializeToStore(),
|
||||
})
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
_, err = c.InsertOne(context.TODO(), d.serializeToStore())
|
||||
if err == nil {
|
||||
d.setExists(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
func doDelete(d *Document, arg interface{}) error {
|
||||
self, ok := arg.(HasID)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf(errFmtNotHasID, nameOf(arg))
|
||||
}
|
||||
c := d.model.Collection()
|
||||
_, err := c.DeleteOne(context.TODO(), bson.M{"_id": self.Id()})
|
||||
if err == nil {
|
||||
d.exists = false
|
||||
err = gridFsDel(arg, *d.model)
|
||||
}
|
||||
return err
|
||||
}
|
||||
func incrementTagged(item interface{}) interface{} {
|
||||
rv := reflect.ValueOf(item)
|
||||
rt := reflect.TypeOf(item)
|
||||
if rv.Kind() != reflect.Pointer {
|
||||
rv = makeSettable(rv, item)
|
||||
}
|
||||
if rt.Kind() == reflect.Pointer {
|
||||
rt = rt.Elem()
|
||||
}
|
||||
if rt.Kind() != reflect.Struct {
|
||||
if rt.Kind() == reflect.Slice {
|
||||
for i := 0; i < rv.Elem().Len(); i++ {
|
||||
incrementTagged(rv.Elem().Index(i).Addr().Interface())
|
||||
}
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
structField := rt.Field(i)
|
||||
cur := rv.Elem().Field(i)
|
||||
tags, err := structtag.Parse(string(structField.Tag))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
incTag, err := tags.Get("autoinc")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
nid := getLastInColl(incTag.Name, cur.Interface())
|
||||
if cur.IsZero() {
|
||||
coerced := coerceInt(reflect.ValueOf(incrementInterface(nid)), cur)
|
||||
if coerced != nil {
|
||||
cur.Set(reflect.ValueOf(coerced))
|
||||
} else {
|
||||
cur.Set(reflect.ValueOf(incrementInterface(nid)))
|
||||
}
|
||||
counterColl := DB.Collection(COUNTER_COL)
|
||||
counterColl.UpdateOne(context.TODO(), bson.M{"collection": incTag.Name}, bson.M{"$set": bson.M{"collection": incTag.Name, "current": cur.Interface()}}, options.UpdateOne().SetUpsert(true))
|
||||
}
|
||||
|
||||
}
|
||||
return rv.Elem().Interface()
|
||||
}
|
||||
|
||||
func incrementAll(item interface{}) {
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
vp := reflect.ValueOf(item)
|
||||
el := vp
|
||||
if vp.Kind() == reflect.Pointer {
|
||||
el = vp.Elem()
|
||||
}
|
||||
if vp.Kind() == reflect.Pointer && vp.IsNil() {
|
||||
return
|
||||
}
|
||||
vt := el.Type()
|
||||
switch el.Kind() {
|
||||
case reflect.Struct:
|
||||
incrementTagged(item)
|
||||
for i := 0; i < el.NumField(); i++ {
|
||||
fv := el.Field(i)
|
||||
fst := vt.Field(i)
|
||||
if !fst.IsExported() {
|
||||
continue
|
||||
}
|
||||
incrementAll(fv.Interface())
|
||||
}
|
||||
case reflect.Slice:
|
||||
for i := 0; i < el.Len(); i++ {
|
||||
incd := incrementTagged(el.Index(i).Addr().Interface())
|
||||
if reflect.ValueOf(incd).Kind() == reflect.Pointer {
|
||||
el.Index(i).Set(reflect.ValueOf(incd).Elem())
|
||||
} else {
|
||||
el.Index(i).Set(reflect.ValueOf(incd))
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Document) markPopulated(field string) {
|
||||
d.newPopulationMap()
|
||||
d.populatedFields[field] = true
|
||||
}
|
||||
|
||||
func (d *Document) markDepopulated(field string) {
|
||||
d.newPopulationMap()
|
||||
d.populatedFields[field] = false
|
||||
}
|
||||
|
||||
func (d *Document) newPopulationMap() {
|
||||
if d.populatedFields == nil {
|
||||
d.populatedFields = make(map[string]bool)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Document) getPopulated() map[string]bool {
|
||||
return d.populatedFields
|
||||
}
|
||||
|
||||
func (d *Document) setRaw(raw any) {
|
||||
d.raw = raw
|
||||
}
|
||||
|
||||
func (d *Document) getRaw() any {
|
||||
return d.raw
|
||||
}
|
47
document_slice.go
Normal file
47
document_slice.go
Normal file
@ -0,0 +1,47 @@
|
||||
package orm
|
||||
|
||||
type IDocumentSlice interface {
|
||||
Delete() error
|
||||
Remove() error
|
||||
Save() error
|
||||
setExists(n bool)
|
||||
getModel() *Model
|
||||
}
|
||||
|
||||
type DocumentSlice []IDocument
|
||||
|
||||
func (d *DocumentSlice) Delete() error {
|
||||
var err error
|
||||
for _, doc := range *d {
|
||||
err = doc.Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DocumentSlice) Remove() error {
|
||||
return d.Delete()
|
||||
}
|
||||
|
||||
func (d *DocumentSlice) Save() error {
|
||||
var err error
|
||||
for _, doc := range *d {
|
||||
err = doc.Save()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DocumentSlice) setExists(b bool) {
|
||||
for _, s := range *d {
|
||||
s.setExists(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DocumentSlice) getModel() *Model {
|
||||
return (*d)[0].getModel()
|
||||
}
|
21
errors.go
Normal file
21
errors.go
Normal file
@ -0,0 +1,21 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
const (
|
||||
errFmtMalformedField = "Malformed field name passed: '%s'"
|
||||
errFmtNotAModel = "Type '%s' is not a model"
|
||||
errFmtNotHasID = "Type '%s' does not implement HasID"
|
||||
errFmtModelNotRegistered = "Model not registered for type: '%s'"
|
||||
)
|
34
go.mod
34
go.mod
@ -3,13 +3,29 @@ module rockfic.com/orm
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/go-loremipsum/loremipsum v1.1.4 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0
|
||||
golang.org/x/net v0.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.1.0
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-loremipsum/loremipsum v1.1.3
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
||||
|
||||
replace rockfic.com/orm => C:/rockfic/orm
|
||||
|
79
go.sum
79
go.sum
@ -1,25 +1,62 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-loremipsum/loremipsum v1.1.4 h1:RJaJlJwX4y9A2+CMgKIyPcjuFHFKTmaNMhxbL+sI6Vg=
|
||||
github.com/go-loremipsum/loremipsum v1.1.4/go.mod h1:whNWskGoefTakPnCu2CO23v5Y7RwiG4LMOEtTDaBeOY=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
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/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/go-loremipsum/loremipsum v1.1.3 h1:ZRhA0ZmJ49lGe5HhWeMONr+iGftWDsHfrYBl5ktDXso=
|
||||
github.com/go-loremipsum/loremipsum v1.1.3/go.mod h1:OJQjXdvwlG9hsyhmMQoT4HOm4DG4l62CYywebw0XBoo=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver/v2 v2.1.0 h1:/ELnVNjmfUKDsoBisXxuJL0noR9CfeUIrP7Yt3R+egg=
|
||||
go.mongodb.org/mongo-driver/v2 v2.1.0/go.mod h1:AWiLRShSrk5RHQS3AEn3RL19rqOzVq49MCpWQ3x/huI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
1229
godoc.html
Normal file
1229
godoc.html
Normal file
File diff suppressed because one or more lines are too long
247
gridfs.go
Normal file
247
gridfs.go
Normal file
@ -0,0 +1,247 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fatih/structtag"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"html/template"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GridFSFile struct {
|
||||
ID bson.ObjectID `bson:"_id"`
|
||||
Name string `bson:"filename"`
|
||||
Length int `bson:"length"`
|
||||
}
|
||||
|
||||
func parseFmt(format string, value any) (string, error) {
|
||||
tmpl, err := template.New("filename").Parse(format)
|
||||
panik(err)
|
||||
w := new(strings.Builder)
|
||||
err = tmpl.Execute(w, value)
|
||||
return w.String(), err
|
||||
}
|
||||
|
||||
func bucket(gfsRef gridFSReference) *mongo.GridFSBucket {
|
||||
b := DB.GridFSBucket(options.GridFSBucket().SetName(gfsRef.BucketName))
|
||||
return b
|
||||
}
|
||||
|
||||
func hasTag(rtype reflect.Type, imodel Model) bool {
|
||||
if rtype.Kind() == reflect.Pointer {
|
||||
rtype = rtype.Elem()
|
||||
}
|
||||
if rtype.Kind() == reflect.Slice {
|
||||
return hasTag(rtype.Elem(), imodel)
|
||||
} else if rtype.Kind() == reflect.Struct {
|
||||
for i := 0; i < rtype.NumField(); i++ {
|
||||
f := rtype.Field(i)
|
||||
tags, err := structtag.Parse(string(f.Tag))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if _, err = tags.Get("gridfs"); err == nil {
|
||||
return true
|
||||
}
|
||||
for kk := range imodel.gridFSReferences {
|
||||
if strings.HasPrefix(kk, f.Name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func gridFsLoad(val any, g gridFSReference, field string) any {
|
||||
doc := reflect.ValueOf(val)
|
||||
rdoc := reflect.ValueOf(val)
|
||||
if doc.Kind() != reflect.Pointer {
|
||||
doc = reflect.New(reflect.TypeOf(val))
|
||||
doc.Elem().Set(reflect.ValueOf(val))
|
||||
}
|
||||
var next string
|
||||
if len(strings.Split(field, ".")) > 1 {
|
||||
next = strings.Join(strings.Split(field, ".")[1:], ".")
|
||||
field = strings.Split(field, ".")[0]
|
||||
} else {
|
||||
next = field
|
||||
}
|
||||
_, rfield, ferr := getNested(field, rdoc)
|
||||
if ferr != nil {
|
||||
return nil
|
||||
}
|
||||
switch rfield.Kind() {
|
||||
case reflect.Slice:
|
||||
for i := 0; i < rfield.Len(); i++ {
|
||||
cur := rfield.Index(i)
|
||||
if cur.Kind() != reflect.Pointer {
|
||||
tmp := reflect.New(cur.Type())
|
||||
tmp.Elem().Set(cur)
|
||||
cur = tmp
|
||||
}
|
||||
intermediate := gridFsLoad(cur.Interface(), g, next)
|
||||
if intermediate == nil {
|
||||
continue
|
||||
}
|
||||
ival := reflect.ValueOf(intermediate)
|
||||
if ival.Kind() == reflect.Pointer {
|
||||
ival = ival.Elem()
|
||||
}
|
||||
rfield.Index(i).Set(ival)
|
||||
}
|
||||
case reflect.Struct:
|
||||
|
||||
intermediate := gridFsLoad(rfield.Interface(), g, next)
|
||||
if intermediate != nil {
|
||||
rfield.Set(reflect.ValueOf(intermediate))
|
||||
}
|
||||
default:
|
||||
b := bucket(g)
|
||||
var found GridFSFile
|
||||
fname, err := parseFmt(g.FilenameFmt, val)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
cursor, err := b.Find(context.TODO(), bson.M{"filename": fname})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
cursor.Next(context.TODO())
|
||||
_ = cursor.Decode(&found)
|
||||
bb := bytes.NewBuffer(nil)
|
||||
_, err = b.DownloadToStream(context.TODO(), found.ID, bb)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if rfield.Type().AssignableTo(reflect.TypeFor[[]byte]()) {
|
||||
rfield.Set(reflect.ValueOf(bb.Bytes()))
|
||||
} else if rfield.Type().AssignableTo(reflect.TypeFor[string]()) {
|
||||
rfield.Set(reflect.ValueOf(bb.String()))
|
||||
}
|
||||
|
||||
}
|
||||
if rdoc.Kind() != reflect.Pointer {
|
||||
return doc.Elem().Interface()
|
||||
}
|
||||
return doc.Interface()
|
||||
}
|
||||
|
||||
func gridFsGen(val any, imodel Model, isSaving bool) error {
|
||||
var rerr error
|
||||
v := reflect.ValueOf(val)
|
||||
el := v
|
||||
if v.Kind() == reflect.Pointer {
|
||||
el = el.Elem()
|
||||
}
|
||||
switch el.Kind() {
|
||||
case reflect.Struct:
|
||||
for i := 0; i < el.NumField(); i++ {
|
||||
ft := el.Type().Field(i)
|
||||
fv := el.Field(i)
|
||||
if !ft.IsExported() {
|
||||
continue
|
||||
}
|
||||
_, err := structtag.Parse(string(ft.Tag))
|
||||
panik(err)
|
||||
var gfsRef *gridFSReference
|
||||
for kk, vv := range imodel.gridFSReferences {
|
||||
if strings.HasPrefix(kk, ft.Name) {
|
||||
gfsRef = &vv
|
||||
break
|
||||
}
|
||||
}
|
||||
var inner = func(b *mongo.GridFSBucket, it reflect.Value) error {
|
||||
filename, err2 := parseFmt(gfsRef.FilenameFmt, it.Interface())
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
contents := GridFSFile{}
|
||||
curs, err2 := b.Find(context.TODO(), bson.M{"filename": filename})
|
||||
|
||||
if !errors.Is(err2, mongo.ErrNoDocuments) {
|
||||
for {
|
||||
if !curs.Next(context.TODO()) {
|
||||
break
|
||||
}
|
||||
_ = curs.Decode(&contents)
|
||||
if !reflect.ValueOf(contents).IsZero() {
|
||||
b.Delete(context.TODO(), contents.ID)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if isSaving {
|
||||
c := it.Field(gfsRef.Idx)
|
||||
var rdr io.Reader
|
||||
|
||||
if c.Type().AssignableTo(reflect.TypeOf([]byte{})) {
|
||||
rdr = bytes.NewReader(c.Interface().([]byte))
|
||||
} else if c.Type().AssignableTo(reflect.TypeOf("")) {
|
||||
rdr = strings.NewReader(c.Interface().(string))
|
||||
} else {
|
||||
return fmt.Errorf("gridfs loader type '%s' not supported", c.Type().String())
|
||||
}
|
||||
_, err = b.UploadFromStream(context.TODO(), filename, rdr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if gfsRef != nil {
|
||||
b := bucket(*gfsRef)
|
||||
if fv.Kind() == reflect.Slice {
|
||||
for j := 0; j < fv.Len(); j++ {
|
||||
lerr := inner(b, fv.Index(j))
|
||||
if lerr != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else if fv.Kind() == reflect.Struct {
|
||||
lerr := inner(b, fv)
|
||||
if lerr != nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
lerr := inner(b, el)
|
||||
if lerr != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasTag(ft.Type, imodel) {
|
||||
err = gridFsGen(fv.Interface(), imodel, isSaving)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
for i := 0; i < el.Len(); i++ {
|
||||
rerr = gridFsGen(el.Index(i).Interface(), imodel, isSaving)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return rerr
|
||||
}
|
||||
|
||||
func gridFsDel(val any, imodel Model) error {
|
||||
return gridFsGen(val, imodel, false)
|
||||
}
|
||||
|
||||
func gridFsSave(val any, imodel Model) error {
|
||||
t := reflect.TypeOf(val)
|
||||
if hasTag(t, imodel) {
|
||||
return gridFsGen(val, imodel, true)
|
||||
}
|
||||
return nil
|
||||
}
|
43
idcounter.go
Normal file
43
idcounter.go
Normal file
@ -0,0 +1,43 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
const COUNTER_COL = "@counters"
|
||||
|
||||
type Counter struct {
|
||||
Current any `bson:"current"`
|
||||
Collection string `bson:"collection"`
|
||||
}
|
||||
|
||||
func getLastInColl(cname string, id interface{}) interface{} {
|
||||
var opts = options.FindOne()
|
||||
|
||||
switch id.(type) {
|
||||
case int, int64, int32, uint, uint32, uint64, bson.ObjectID:
|
||||
opts.SetSort(bson.M{"_id": -1})
|
||||
case string:
|
||||
opts.SetSort(bson.M{"createdAt": -1})
|
||||
default:
|
||||
panic(ErrUnsupportedID)
|
||||
}
|
||||
|
||||
var cnt Counter
|
||||
if !reflect.ValueOf(id).IsZero() {
|
||||
return id
|
||||
}
|
||||
if err := DB.Collection(COUNTER_COL).FindOne(context.TODO(), bson.M{
|
||||
"collection": cname,
|
||||
}, opts).Decode(&cnt); err != nil {
|
||||
cnt = Counter{
|
||||
Current: id,
|
||||
}
|
||||
cnt.Collection = cname
|
||||
}
|
||||
return cnt.Current
|
||||
}
|
148
indexes.go
Normal file
148
indexes.go
Normal file
@ -0,0 +1,148 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
var optionKeywords = [...]string{"unique", "sparse", "background", "dropdups"}
|
||||
|
||||
// prolly won't need to use indexes, but just in case...
|
||||
type InternalIndex struct {
|
||||
Fields []string
|
||||
Options []string
|
||||
lastFieldIdx int
|
||||
sticky bool
|
||||
}
|
||||
|
||||
func (in *InternalIndex) appendOption(o string) {
|
||||
o = strings.ToLower(o)
|
||||
o = strings.Trim(o, " ")
|
||||
for i := range optionKeywords {
|
||||
if optionKeywords[i] == o {
|
||||
in.Options = append(in.Options, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (in *InternalIndex) appendField(f string) {
|
||||
in.Fields = append(in.Fields, f)
|
||||
}
|
||||
|
||||
func (in *InternalIndex) appendDotField(f string) {
|
||||
in.Fields[in.lastFieldIdx] = in.Fields[in.lastFieldIdx] + "." + f
|
||||
}
|
||||
|
||||
func (in *InternalIndex) updateLastField() {
|
||||
if len(in.Fields) > 0 {
|
||||
in.lastFieldIdx = len(in.Fields) - 1
|
||||
return
|
||||
}
|
||||
in.lastFieldIdx = 0
|
||||
}
|
||||
|
||||
func (in *InternalIndex) setSticky(s bool) {
|
||||
in.sticky = s
|
||||
}
|
||||
|
||||
func (in *InternalIndex) getSticky() bool {
|
||||
return in.sticky
|
||||
}
|
||||
|
||||
func scanIndex(src string) []InternalIndex {
|
||||
var s scanner.Scanner
|
||||
var parsed []InternalIndex
|
||||
|
||||
src = func(ss string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, ss)
|
||||
}(src)
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile("", fset.Base(), len(src))
|
||||
s.Init(file, []byte(src), nil, scanner.ScanComments)
|
||||
|
||||
lb := false
|
||||
|
||||
p := &InternalIndex{}
|
||||
|
||||
for {
|
||||
_, tok, lit := s.Scan()
|
||||
|
||||
switch tok {
|
||||
case token.LBRACE:
|
||||
if lb {
|
||||
goto _panik
|
||||
}
|
||||
lb = true
|
||||
case token.RBRACE:
|
||||
if !lb || len(p.Fields) == 0 {
|
||||
goto _panik
|
||||
}
|
||||
lb = false
|
||||
case token.IDENT:
|
||||
if lb {
|
||||
if p.getSticky() {
|
||||
p.appendDotField(lit)
|
||||
p.setSticky(false)
|
||||
break
|
||||
}
|
||||
p.appendField(lit)
|
||||
break
|
||||
}
|
||||
p.appendOption(lit)
|
||||
case token.PERIOD:
|
||||
if p.getSticky() {
|
||||
goto _panik
|
||||
}
|
||||
p.setSticky(true)
|
||||
p.updateLastField()
|
||||
case token.COMMA:
|
||||
case token.COLON:
|
||||
if lb {
|
||||
goto _panik
|
||||
}
|
||||
case token.SEMICOLON:
|
||||
if lb {
|
||||
goto _panik
|
||||
}
|
||||
parsed = append(parsed, *p)
|
||||
p = &InternalIndex{}
|
||||
case token.EOF:
|
||||
if lb {
|
||||
goto _panik
|
||||
}
|
||||
return parsed
|
||||
default:
|
||||
goto _panik
|
||||
}
|
||||
}
|
||||
|
||||
_panik:
|
||||
panic("parsing error in index expression!")
|
||||
}
|
||||
|
||||
func buildIndex(i InternalIndex) *mongo.IndexModel {
|
||||
idx := &mongo.IndexModel{
|
||||
Keys: i.Fields,
|
||||
}
|
||||
|
||||
for _, o := range i.Options {
|
||||
switch o {
|
||||
case "unique":
|
||||
idx.Options.SetUnique(true)
|
||||
//case "background":
|
||||
// idx.Options.SetBackground(true)
|
||||
case "sparse":
|
||||
idx.Options.SetSparse(true)
|
||||
}
|
||||
}
|
||||
return idx
|
||||
}
|
270
model.go
Normal file
270
model.go
Normal file
@ -0,0 +1,270 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Model - type which contains "static" methods like
|
||||
// Find, FindOne, etc.
|
||||
type Model struct {
|
||||
Indexes map[string][]InternalIndex
|
||||
Type reflect.Type
|
||||
collection string
|
||||
gridFSReferences map[string]gridFSReference
|
||||
idx int
|
||||
references map[string]Reference
|
||||
typeName string `bson:"-"`
|
||||
}
|
||||
|
||||
// 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 []HasID
|
||||
|
||||
type IModel interface {
|
||||
FindRaw(query interface{}, opts *options.FindOptionsBuilder) (*mongo.Cursor, error)
|
||||
Find(query interface{}, opts *options.FindOptionsBuilder) (*Query, error)
|
||||
FindByID(id interface{}) (*Query, error)
|
||||
FindOne(query interface{}, options *options.FindOneOptionsBuilder) (*Query, error)
|
||||
FindPaged(query interface{}, page int64, perPage int64, options *options.FindOptionsBuilder) (*Query, error)
|
||||
Exists(query interface{}) (bool, error)
|
||||
ExistsID(id interface{}) (bool, error)
|
||||
Collection() *mongo.Collection
|
||||
|
||||
getIdxs() []*mongo.IndexModel
|
||||
getParsedIdxs() map[string][]InternalIndex
|
||||
getTypeName() string
|
||||
setTypeName(str string)
|
||||
}
|
||||
|
||||
func (m *Model) getTypeName() string {
|
||||
return m.typeName
|
||||
}
|
||||
|
||||
func (m *Model) setTypeName(str string) {
|
||||
m.typeName = str
|
||||
}
|
||||
|
||||
// Collection - returns the collection associated with this Model
|
||||
func (m *Model) Collection() *mongo.Collection {
|
||||
return DB.Collection(m.collection)
|
||||
}
|
||||
|
||||
func (m *Model) getIdxs() []*mongo.IndexModel {
|
||||
mi := make([]*mongo.IndexModel, 0)
|
||||
if mpi := m.getParsedIdxs(); mpi != nil {
|
||||
for _, v := range mpi {
|
||||
for _, i := range v {
|
||||
mi = append(mi, buildIndex(i))
|
||||
}
|
||||
}
|
||||
return mi
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) getParsedIdxs() map[string][]InternalIndex {
|
||||
_, ri, ok := ModelRegistry.HasByName(m.typeName)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf(errFmtModelNotRegistered, m.typeName))
|
||||
}
|
||||
return ri.Indexes
|
||||
}
|
||||
|
||||
// FindRaw - find documents satisfying `query` and return a plain mongo cursor.
|
||||
func (m *Model) FindRaw(query interface{}, opts *options.FindOptionsBuilder) (*mongo.Cursor, error) {
|
||||
coll := m.Collection()
|
||||
if opts == nil {
|
||||
opts = options.Find()
|
||||
}
|
||||
var fo options.FindOptions
|
||||
for _, setter := range opts.Opts {
|
||||
_ = setter(&fo)
|
||||
}
|
||||
cursor, err := coll.Find(context.TODO(), query, opts)
|
||||
return cursor, err
|
||||
}
|
||||
|
||||
// Find - find all documents satisfying `query`.
|
||||
// returns a pointer to a Query for further chaining.
|
||||
func (m *Model) Find(query interface{}, opts *options.FindOptionsBuilder) (*Query, error) {
|
||||
qqn := ModelRegistry.new_(m.typeName)
|
||||
qqt := reflect.SliceOf(reflect.TypeOf(qqn))
|
||||
qqv := reflect.New(qqt)
|
||||
qqv.Elem().Set(reflect.MakeSlice(qqt, 0, 0))
|
||||
qq := &Query{
|
||||
model: m,
|
||||
collection: m.Collection(),
|
||||
doc: qqv.Interface(),
|
||||
op: OP_FIND_ALL,
|
||||
}
|
||||
q, err := m.FindRaw(query, opts)
|
||||
//idoc := (*DocumentSlice)(qqv.Elem().UnsafePointer())
|
||||
idoc := make(DocumentSlice, 0)
|
||||
|
||||
if err == nil {
|
||||
rawRes := bson.A{}
|
||||
err = q.All(context.TODO(), &rawRes)
|
||||
if err == nil {
|
||||
idoc.setExists(true)
|
||||
}
|
||||
|
||||
qq.rawDoc = rawRes
|
||||
err = q.All(context.TODO(), &qq.doc)
|
||||
if err != nil {
|
||||
qq.reOrganize()
|
||||
err = nil
|
||||
}
|
||||
for i := 0; i < qqv.Elem().Len(); i++ {
|
||||
idoc = append(idoc, qqv.Elem().Index(i).Interface().(IDocument))
|
||||
}
|
||||
for i, doc := range idoc {
|
||||
doc.setModel(*m)
|
||||
doc.SetSelf(doc)
|
||||
doc.setRaw(rawRes[i])
|
||||
}
|
||||
}
|
||||
|
||||
return qq, err
|
||||
}
|
||||
|
||||
// FindPaged - Wrapper around FindAll with the Skip and Limit options populated.
|
||||
// returns a pointer to a Query for further chaining.
|
||||
func (m *Model) FindPaged(query interface{}, page int64, perPage int64, opts *options.FindOptionsBuilder) (*Query, error) {
|
||||
skipAmt := perPage * (page - 1)
|
||||
if skipAmt < 0 {
|
||||
skipAmt = 0
|
||||
}
|
||||
opts.SetSkip(skipAmt).SetLimit(perPage)
|
||||
q, err := m.Find(query, opts)
|
||||
q.op = OP_FIND_PAGED
|
||||
return q, err
|
||||
}
|
||||
|
||||
// FindByID - find a single document by its _id field.
|
||||
// Wrapper around FindOne with an ID query as its first argument
|
||||
func (m *Model) FindByID(id interface{}) (*Query, error) {
|
||||
return m.FindOne(bson.D{{"_id", id}}, nil)
|
||||
}
|
||||
|
||||
// FindOne - find a single document satisfying `query`.
|
||||
// returns a pointer to a Query for further chaining.
|
||||
func (m *Model) FindOne(query interface{}, options *options.FindOneOptionsBuilder) (*Query, error) {
|
||||
coll := m.Collection()
|
||||
rip := coll.FindOne(context.TODO(), query, options)
|
||||
raw := bson.M{}
|
||||
err := rip.Decode(&raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qqn := ModelRegistry.new_(m.typeName)
|
||||
idoc, ok := qqn.(IDocument)
|
||||
idoc.setRaw(raw)
|
||||
|
||||
qq := &Query{
|
||||
collection: m.Collection(),
|
||||
rawDoc: raw,
|
||||
doc: idoc,
|
||||
op: OP_FIND_ONE,
|
||||
model: m,
|
||||
}
|
||||
qq.rawDoc = raw
|
||||
err = rip.Decode(qq.doc)
|
||||
if err != nil {
|
||||
qq.reOrganize()
|
||||
err = nil
|
||||
}
|
||||
if ok {
|
||||
idoc.setExists(true)
|
||||
idoc.setModel(*m)
|
||||
idoc.setRaw(raw)
|
||||
}
|
||||
idoc.SetSelf(idoc)
|
||||
return qq, err
|
||||
}
|
||||
|
||||
func (m *Model) Count(query interface{}, options *options.CountOptionsBuilder) (int64, error) {
|
||||
coll := m.Collection()
|
||||
return coll.CountDocuments(context.TODO(), query, options)
|
||||
}
|
||||
|
||||
func (m *Model) Exists(query interface{}) (bool, error) {
|
||||
cnt, err := m.Count(query, options.Count())
|
||||
return cnt > 0, err
|
||||
}
|
||||
|
||||
func (m *Model) ExistsID(id interface{}) (bool, error) {
|
||||
cnt, err := m.Count(bson.M{"_id": id}, options.Count())
|
||||
return cnt > 0, err
|
||||
}
|
||||
|
||||
func createBase(d any) (reflect.Value, int, string) {
|
||||
var n string
|
||||
var ri *Model
|
||||
var ok bool
|
||||
|
||||
n, ri, ok = ModelRegistry.HasByName(nameOf(d))
|
||||
|
||||
if !ok {
|
||||
ModelRegistry.Model(d)
|
||||
n, ri, _ = ModelRegistry.Has(d)
|
||||
}
|
||||
t := ri.Type
|
||||
v := valueOf(d)
|
||||
i := ModelRegistry.Index(n)
|
||||
|
||||
r := reflect.New(reflect.PointerTo(t)).Elem()
|
||||
r.Addr().Elem().Set(reflect.New(t))
|
||||
|
||||
r.Addr().Elem().Elem().Set(v)
|
||||
|
||||
if reflect.ValueOf(d).Kind() == reflect.Pointer {
|
||||
r.Addr().Elem().Elem().Set(reflect.ValueOf(d).Elem())
|
||||
} else {
|
||||
r.Addr().Elem().Elem().Set(reflect.ValueOf(d))
|
||||
}
|
||||
ri.setTypeName(n)
|
||||
r.Interface().(IDocument).setModel(*ri)
|
||||
r.Interface().(IDocument).newPopulationMap()
|
||||
|
||||
return r, i, n
|
||||
}
|
||||
|
||||
// Create creates a new instance of a given Document
|
||||
// type and returns a pointer to it.
|
||||
func Create(d any) any {
|
||||
r, _, n := createBase(d)
|
||||
//df := r.Elem().Field(i)
|
||||
dm := r.Interface().(IDocument)
|
||||
dm.getModel().setTypeName(n)
|
||||
what := r.Interface()
|
||||
|
||||
dm.SetSelf(what)
|
||||
//df.Set(reflect.ValueOf(dm))
|
||||
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()
|
||||
rslice := reflect.SliceOf(rtype)
|
||||
newItem := reflect.New(rslice)
|
||||
newItem.Elem().Set(reflect.MakeSlice(rslice, 0, 0))
|
||||
return newItem.Elem().Interface().([]*T)
|
||||
}
|
281
model_test.go
Normal file
281
model_test.go
Normal file
@ -0,0 +1,281 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
initTest()
|
||||
is := iti_single()
|
||||
doc := Create(is).(*story)
|
||||
assert.Equal(t, is.Title, doc.Title)
|
||||
assert.Equal(t, is.Chapters[0].Summary, doc.Chapters[0].Summary)
|
||||
}
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
initTest()
|
||||
storyDoc := Create(iti_multi()).(*story)
|
||||
lauthor := Create(author).(*user)
|
||||
storyDoc.Author = lauthor
|
||||
assert.Equal(t, storyDoc.Id(), int64(0))
|
||||
assert.Equal(t, lauthor.ID, storyDoc.Author.ID)
|
||||
aerr := lauthor.Save()
|
||||
assert.Equal(t, nil, aerr)
|
||||
serr := storyDoc.Save()
|
||||
assert.Equal(t, nil, serr)
|
||||
assert.Less(t, int64(0), storyDoc.ID)
|
||||
assert.Less(t, int64(0), lauthor.ID)
|
||||
for _, c := range storyDoc.Chapters {
|
||||
assert.NotZero(t, c.ChapterID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopulate(t *testing.T) {
|
||||
initTest()
|
||||
|
||||
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
|
||||
storyDoc := Create(iti_single()).(*story)
|
||||
mauthor := Create(author).(*user)
|
||||
saveDoc(t, mauthor)
|
||||
saveDoc(t, bandDoc)
|
||||
storyDoc.Author = mauthor
|
||||
saveDoc(t, storyDoc)
|
||||
assert.Greater(t, storyDoc.ID, int64(0))
|
||||
|
||||
smodel := ModelRegistry["story"]
|
||||
q, err := smodel.FindByID(storyDoc.ID)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotPanics(t, func() {
|
||||
foundDoc := &story{}
|
||||
q.Populate("Author", "Chapters.Bands").Exec(foundDoc)
|
||||
j, _ := q.JSON()
|
||||
fmt.Printf("%s\n", j)
|
||||
})
|
||||
for _, c := range storyDoc.Chapters {
|
||||
assert.NotZero(t, c.Bands[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
initTest()
|
||||
nb := Create(metallica).(*band)
|
||||
saveDoc(t, nb)
|
||||
nb.Locked = true
|
||||
saveDoc(t, nb)
|
||||
|
||||
foundM := ModelRegistry["band"]
|
||||
q, err := foundM.FindByID(int64(1))
|
||||
assert.Equal(t, nil, err)
|
||||
found := &band{}
|
||||
q.Exec(found)
|
||||
assert.Equal(t, int64(1), found.ID)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, true, found.Locked)
|
||||
}
|
||||
|
||||
func TestModel_FindAll(t *testing.T) {
|
||||
initTest()
|
||||
im := iti_multi()
|
||||
createAndSave(t, &im)
|
||||
smodel := ModelRegistry["story"]
|
||||
query, err := smodel.Find(bson.M{}, options.Find())
|
||||
assert.Equal(t, nil, err)
|
||||
final := CreateSlice(story{})
|
||||
query.Exec(&final)
|
||||
assert.Greater(t, len(final), 0)
|
||||
}
|
||||
|
||||
func TestModel_PopulateMulti(t *testing.T) {
|
||||
initTest()
|
||||
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
|
||||
saveDoc(t, bandDoc)
|
||||
mauthor := Create(author).(*user)
|
||||
saveDoc(t, mauthor)
|
||||
im := iti_multi()
|
||||
im.Author = mauthor
|
||||
createAndSave(t, &im)
|
||||
smodel := ModelRegistry["story"]
|
||||
query, err := smodel.Find(bson.M{}, options.Find())
|
||||
assert.Equal(t, nil, err)
|
||||
final := CreateSlice(story{})
|
||||
query.Populate("Author", "Chapters.Bands").Exec(&final)
|
||||
assert.Greater(t, len(final), 0)
|
||||
for _, s := range final {
|
||||
assert.NotZero(t, s.Chapters[0].Bands[0].Name)
|
||||
}
|
||||
bytes, _ := json.MarshalIndent(final, "", "\t")
|
||||
fmt.Println(string(bytes))
|
||||
}
|
||||
|
||||
func TestModel_PopulateChained_Multi(t *testing.T) {
|
||||
initTest()
|
||||
im := iti_multi()
|
||||
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
|
||||
saveDoc(t, bandDoc)
|
||||
mauthor := Create(author).(*user)
|
||||
saveDoc(t, mauthor)
|
||||
im.Author = mauthor
|
||||
createAndSave(t, &im)
|
||||
smodel := ModelRegistry["story"]
|
||||
query, err := smodel.Find(bson.M{}, options.Find())
|
||||
assert.Equal(t, nil, err)
|
||||
final := CreateSlice(story{})
|
||||
query.Populate("Author").Populate("Chapters.Bands").Exec(&final)
|
||||
assert.Greater(t, len(final), 0)
|
||||
for _, s := range final {
|
||||
assert.NotZero(t, s.Chapters[0].Bands[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopulate_Chained(t *testing.T) {
|
||||
initTest()
|
||||
|
||||
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
|
||||
storyDoc := Create(iti_single()).(*story)
|
||||
mauthor := Create(author).(*user)
|
||||
saveDoc(t, mauthor)
|
||||
saveDoc(t, bandDoc)
|
||||
storyDoc.Author = mauthor
|
||||
saveDoc(t, storyDoc)
|
||||
assert.Greater(t, storyDoc.ID, int64(0))
|
||||
|
||||
smodel := ModelRegistry["story"]
|
||||
q, err := smodel.FindByID(storyDoc.ID)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotPanics(t, func() {
|
||||
foundDoc := &story{}
|
||||
q.Populate("Author").Populate("Chapters.Bands").Exec(foundDoc)
|
||||
j, _ := q.JSON()
|
||||
fmt.Printf("%s\n", j)
|
||||
})
|
||||
for _, c := range storyDoc.Chapters {
|
||||
assert.NotZero(t, c.Bands[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModel_Append(t *testing.T) {
|
||||
initTest()
|
||||
bandDoc := Create(metallica).(*band)
|
||||
saveDoc(t, bandDoc)
|
||||
bmodel := ModelRegistry["band"]
|
||||
query, err := bmodel.FindByID(int64(1))
|
||||
assert.Equal(t, nil, err)
|
||||
fin := &band{}
|
||||
query.Exec(fin)
|
||||
assert.Greater(t, fin.ID, int64(0))
|
||||
err = fin.Append("Characters", "Robert Trujillo")
|
||||
assert.Equal(t, nil, err)
|
||||
saveDoc(t, fin)
|
||||
fin = &band{}
|
||||
query, _ = bmodel.FindByID(int64(1))
|
||||
query.Exec(fin)
|
||||
assert.Greater(t, len(fin.Characters), 4)
|
||||
}
|
||||
|
||||
func TestModel_Delete(t *testing.T) {
|
||||
initTest()
|
||||
bandDoc := Create(metallica).(*band)
|
||||
saveDoc(t, bandDoc)
|
||||
err := bandDoc.Delete()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestModel_Pull(t *testing.T) {
|
||||
initTest()
|
||||
storyDoc := Create(iti_multi()).(*story)
|
||||
smodel := ModelRegistry["story"]
|
||||
saveDoc(t, storyDoc)
|
||||
err := storyDoc.Pull("Chapters", storyDoc.Chapters[4])
|
||||
assert.Nil(t, err)
|
||||
assert.NotZero(t, storyDoc.ID)
|
||||
saveDoc(t, storyDoc)
|
||||
fin := &story{}
|
||||
query, err := smodel.FindByID(storyDoc.ID)
|
||||
assert.Nil(t, err)
|
||||
query.Exec(fin)
|
||||
assert.Equal(t, 4, len(fin.Chapters))
|
||||
}
|
||||
|
||||
func TestModel_Swap(t *testing.T) {
|
||||
initTest()
|
||||
is := iti_single()
|
||||
is.Author = &author
|
||||
storyDoc := Create(iti_single()).(*story)
|
||||
saveDoc(t, storyDoc)
|
||||
storyDoc.Chapters[0].Bands = append(storyDoc.Chapters[0].Bands, bodom)
|
||||
assert.Equal(t, 2, len(storyDoc.Chapters[0].Bands))
|
||||
err := storyDoc.Swap("Chapters[0].Bands", 0, 1)
|
||||
assert.Nil(t, err)
|
||||
c := storyDoc.Chapters[0].Bands
|
||||
assert.Equal(t, bodom.ID, c[0].ID)
|
||||
assert.Equal(t, diamondHead.ID, c[1].ID)
|
||||
saveDoc(t, storyDoc)
|
||||
}
|
||||
|
||||
func TestModel_GridFSLoad(t *testing.T) {
|
||||
initTest()
|
||||
ModelRegistry.Model(somethingWithNestedChapters{})
|
||||
model := ModelRegistry["somethingWithNestedChapters"]
|
||||
thingDoc := Create(doSomethingWithNested()).(*somethingWithNestedChapters)
|
||||
found := &somethingWithNestedChapters{}
|
||||
|
||||
saveDoc(t, thingDoc)
|
||||
assert.NotZero(t, thingDoc.ID)
|
||||
fq, err := model.FindByID(thingDoc.ID)
|
||||
assert.Nil(t, err)
|
||||
fq.LoadFile("NestedText", "Chapters.Text").Exec(found)
|
||||
assert.NotZero(t, found.NestedText)
|
||||
assert.NotZero(t, len(found.Chapters))
|
||||
for _, c := range found.Chapters {
|
||||
assert.NotZero(t, c.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModel_GridFSLoad_Chained(t *testing.T) {
|
||||
initTest()
|
||||
ModelRegistry.Model(somethingWithNestedChapters{})
|
||||
model := ModelRegistry["somethingWithNestedChapters"]
|
||||
thingDoc := Create(doSomethingWithNested()).(*somethingWithNestedChapters)
|
||||
found := &somethingWithNestedChapters{}
|
||||
|
||||
saveDoc(t, thingDoc)
|
||||
assert.NotZero(t, thingDoc.ID)
|
||||
fq, err := model.FindByID(thingDoc.ID)
|
||||
assert.Nil(t, err)
|
||||
fq.LoadFile("NestedText").LoadFile("Chapters.Text").Exec(found)
|
||||
assert.NotZero(t, found.NestedText)
|
||||
assert.NotZero(t, len(found.Chapters))
|
||||
for _, c := range found.Chapters {
|
||||
assert.NotZero(t, c.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModel_GridFSLoad_Complex(t *testing.T) {
|
||||
initTest()
|
||||
model := ModelRegistry["story"]
|
||||
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
|
||||
thingDoc := Create(iti_multi()).(*story)
|
||||
mauthor := Create(author).(*user)
|
||||
found := &story{}
|
||||
saveDoc(t, bandDoc)
|
||||
saveDoc(t, mauthor)
|
||||
thingDoc.Author = mauthor
|
||||
saveDoc(t, thingDoc)
|
||||
assert.NotZero(t, thingDoc.ID)
|
||||
fq, err := model.FindByID(thingDoc.ID)
|
||||
assert.Nil(t, err)
|
||||
fq.Populate("Author", "Chapters.Bands").LoadFile("Chapters.Text").Exec(found)
|
||||
assert.NotZero(t, len(found.Chapters))
|
||||
for _, c := range found.Chapters {
|
||||
assert.NotZero(t, c.Text)
|
||||
assert.NotZero(t, c.Bands[0].Name)
|
||||
}
|
||||
j, _ := fq.JSON()
|
||||
fmt.Printf("%s\n", j)
|
||||
}
|
638
query.go
Normal file
638
query.go
Normal file
@ -0,0 +1,638 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fatih/structtag"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Query struct {
|
||||
collection *mongo.Collection
|
||||
op string
|
||||
model *Model
|
||||
done bool
|
||||
rawDoc any
|
||||
doc any
|
||||
}
|
||||
|
||||
const (
|
||||
OP_FIND_ONE = "findOne"
|
||||
OP_FIND_PAGED = "findPaged"
|
||||
OP_FIND_ALL = "findAll"
|
||||
OP_FIND = "find"
|
||||
)
|
||||
|
||||
func populate(r Reference,
|
||||
alreadyPopulated map[string]bool,
|
||||
rcoll string, rawDoc interface{},
|
||||
curDescent string, src interface{}) any {
|
||||
rt := reflect.TypeOf(src)
|
||||
rv := reflect.ValueOf(src)
|
||||
srt := rt
|
||||
if srt.Kind() == reflect.Pointer {
|
||||
srt = rt.Elem()
|
||||
}
|
||||
if rv.Kind() == reflect.Pointer && reflect.ValueOf(src).IsNil() {
|
||||
return src
|
||||
}
|
||||
if rv.Kind() != reflect.Pointer {
|
||||
rv = reflect.New(rt)
|
||||
rv.Elem().Set(reflect.ValueOf(src))
|
||||
}
|
||||
if srt.Kind() == reflect.Struct && !isObject(rawDoc) {
|
||||
nrd := ModelRegistry.Get(srt.Name())
|
||||
if nrd != nil && nrd.collection != rcoll {
|
||||
q, err := nrd.FindByID(rawDoc)
|
||||
if err == nil {
|
||||
rawDoc = q.rawDoc
|
||||
toPopulate := []string{curDescent}
|
||||
if asIDoc, ok := rv.Interface().(IDocument); ok {
|
||||
for k, v := range asIDoc.getPopulated() {
|
||||
if k != curDescent && v {
|
||||
toPopulate = append(toPopulate, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
q.Populate(toPopulate...)
|
||||
|
||||
q.Exec(rv.Interface())
|
||||
src = rv.Interface()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fieldsMap [3]string
|
||||
type bsonWhat struct {
|
||||
What string
|
||||
}
|
||||
var isNotStructOrSlice bool
|
||||
var w bsonWhat
|
||||
switch rawDoc.(type) {
|
||||
case bson.A:
|
||||
w.What = "A"
|
||||
case bson.D:
|
||||
w.What = "D"
|
||||
case bson.M:
|
||||
w.What = "M"
|
||||
default:
|
||||
w.What = "-"
|
||||
}
|
||||
var fld string
|
||||
var next string
|
||||
if len(strings.Split(curDescent, ".")) > 1 {
|
||||
next = strings.Join(strings.Split(curDescent, ".")[1:], ".")
|
||||
fld = strings.Split(curDescent, ".")[0]
|
||||
} else {
|
||||
fld = curDescent
|
||||
next = curDescent
|
||||
}
|
||||
var toReturn interface{}
|
||||
switch w.What {
|
||||
case "A":
|
||||
rvs := reflect.MakeSlice(rt, 0, 0)
|
||||
var rahh reflect.Value
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rahh = rv.Elem()
|
||||
} else {
|
||||
rahh = rv
|
||||
}
|
||||
if len(rawDoc.(bson.A)) > 0 {
|
||||
if !isObject(rawDoc.(bson.A)[0]) {
|
||||
next = curDescent
|
||||
}
|
||||
}
|
||||
|
||||
for i, el := range rawDoc.(bson.A) {
|
||||
it := rahh.Index(i)
|
||||
rel := el
|
||||
popped := populate(r, alreadyPopulated, rcoll, rel, next, it.Interface())
|
||||
if pidoc, pok := popped.(IDocument); pok {
|
||||
pidoc.setRaw(rel)
|
||||
}
|
||||
poppedVal := reflect.ValueOf(popped)
|
||||
if poppedVal.Kind() == reflect.Pointer {
|
||||
rvs = reflect.Append(rvs, poppedVal.Elem())
|
||||
} else {
|
||||
rvs = reflect.Append(rvs, poppedVal)
|
||||
}
|
||||
}
|
||||
if rv.CanSet() {
|
||||
rv.Set(rvs)
|
||||
} else if rv.Kind() == reflect.Pointer {
|
||||
rv.Elem().Set(rvs)
|
||||
} else {
|
||||
src = rvs.Interface()
|
||||
toReturn = src
|
||||
}
|
||||
case "D":
|
||||
loc := rawDoc.(bson.D)
|
||||
nrd := bson.M{}
|
||||
for _, el := range loc {
|
||||
nrd[el.Key] = el.Value
|
||||
}
|
||||
rawDoc = nrd
|
||||
fallthrough
|
||||
case "M":
|
||||
dd := rawDoc.(bson.M)
|
||||
var sf reflect.Value
|
||||
var rsf reflect.Value
|
||||
if rv.Kind() == reflect.Pointer {
|
||||
sf = rv.Elem().FieldByName(fld)
|
||||
} else {
|
||||
sf = rv.FieldByName(fld)
|
||||
}
|
||||
if rv.Kind() == reflect.Pointer {
|
||||
rsf = rv.Elem().FieldByName(fld)
|
||||
} else {
|
||||
rsf = rv.FieldByName(fld)
|
||||
}
|
||||
|
||||
var ff reflect.StructField
|
||||
var ok bool
|
||||
if rt.Kind() == reflect.Pointer {
|
||||
ff, ok = rt.Elem().FieldByName(fld)
|
||||
} else {
|
||||
ff, ok = rt.FieldByName(fld)
|
||||
}
|
||||
|
||||
if ok {
|
||||
tag, err := structtag.Parse(string(ff.Tag))
|
||||
if err == nil {
|
||||
val, err2 := tag.Get("bson")
|
||||
if err2 == nil && val.Name != "-" {
|
||||
fttt := ff.Type
|
||||
if fttt.Kind() == reflect.Pointer || fttt.Kind() == reflect.Slice {
|
||||
fttt = fttt.Elem()
|
||||
}
|
||||
fieldsMap = [3]string{fld, fttt.Name(), val.Name}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("todo")
|
||||
}
|
||||
intermediate := populate(r, alreadyPopulated, rcoll, dd[fieldsMap[2]], next, sf.Interface())
|
||||
/*if iidoc, idocOk := intermediate.(IDocument); idocOk {
|
||||
if (reflect.ValueOf(intermediate).CanAddr() && !reflect.ValueOf(intermediate).IsNil()) || !reflect.ValueOf(intermediate).IsZero() {
|
||||
iiid, iok := intermediate.(HasID)
|
||||
if intermediate != nil && iok && !reflect.ValueOf(iiid.Id()).IsZero() {
|
||||
iidoc.setRaw(dd[fieldsMap[2]])
|
||||
}
|
||||
}
|
||||
}*/
|
||||
if rsf.CanSet() {
|
||||
ival := reflect.ValueOf(intermediate)
|
||||
if ival.Kind() != reflect.Pointer && rsf.Kind() == reflect.Pointer {
|
||||
rsf.Set(ival.Elem())
|
||||
} else if ival.Kind() == reflect.Pointer && rsf.Kind() != reflect.Pointer {
|
||||
rsf.Set(ival.Elem())
|
||||
} else {
|
||||
rsf.Set(ival)
|
||||
}
|
||||
} else {
|
||||
src = intermediate
|
||||
}
|
||||
default:
|
||||
isNotStructOrSlice = true
|
||||
|
||||
if r.exists {
|
||||
tto := r.HydratedType
|
||||
if tto.Kind() == reflect.Pointer || tto.Kind() == reflect.Slice {
|
||||
tto = tto.Elem()
|
||||
}
|
||||
|
||||
rawt := ModelRegistry.new_(tto.Name())
|
||||
t := rawt.(IDocument)
|
||||
q := bson.M{"_id": rawDoc}
|
||||
reso := DB.Collection(rcoll).FindOne(context.TODO(), q)
|
||||
if !errors.Is(reso.Err(), mongo.ErrNoDocuments) {
|
||||
var anotherMap = make(bson.M)
|
||||
reso.Decode(&anotherMap)
|
||||
reflect.ValueOf(t).Elem().Set(reflect.ValueOf(rerere(anotherMap, tto, false)).Elem())
|
||||
t.setRaw(anotherMap)
|
||||
}
|
||||
hatred := rv
|
||||
if hatred.Kind() == reflect.Pointer {
|
||||
hatred = hatred.Elem()
|
||||
}
|
||||
if hatred.CanSet() {
|
||||
if reflect.ValueOf(t).Kind() == reflect.Pointer {
|
||||
if hatred.Kind() == reflect.Pointer {
|
||||
hatred.Set(reflect.ValueOf(t))
|
||||
} else {
|
||||
hatred.Set(reflect.ValueOf(t).Elem())
|
||||
}
|
||||
} else {
|
||||
hatred.Set(reflect.ValueOf(t))
|
||||
}
|
||||
} else {
|
||||
src = t
|
||||
toReturn = src
|
||||
}
|
||||
t.SetSelf(t)
|
||||
t.setExists(true)
|
||||
}
|
||||
}
|
||||
|
||||
if toReturn == nil {
|
||||
sidoc, sok := rv.Interface().(IDocument)
|
||||
if sok {
|
||||
sidoc.SetSelf(rv.Interface())
|
||||
if !isNotStructOrSlice {
|
||||
sidoc.setRaw(rawDoc)
|
||||
}
|
||||
} else if rv.Kind() == reflect.Pointer && rt.Kind() != reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
return rv.Interface()
|
||||
}
|
||||
sidoc, sok := src.(IDocument)
|
||||
if sok {
|
||||
sidoc.SetSelf(src)
|
||||
if !isNotStructOrSlice {
|
||||
sidoc.setRaw(rawDoc)
|
||||
}
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
// 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 (q *Query) LoadFile(fields ...string) *Query {
|
||||
_, cm, _ := ModelRegistry.HasByName(q.model.typeName)
|
||||
if cm != nil {
|
||||
for _, field := range fields {
|
||||
var r gridFSReference
|
||||
hasAnnotated := false
|
||||
for k2, v := range cm.gridFSReferences {
|
||||
if strings.HasPrefix(k2, field) {
|
||||
r = v
|
||||
hasAnnotated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasAnnotated {
|
||||
q.doc = gridFsLoad(q.doc, r, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func readFields(field string, m *Model) map[string]Reference {
|
||||
r := make(map[string]Reference)
|
||||
if m == nil {
|
||||
return r
|
||||
}
|
||||
for k, v := range m.references {
|
||||
if strings.HasPrefix(field, k) {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
if vv, ok := m.references[field]; ok {
|
||||
r[field] = vv
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Populate populates document references via reflection
|
||||
func (q *Query) Populate(fields ...string) *Query {
|
||||
vvv := reflect.ValueOf(q.doc)
|
||||
if vvv.Kind() == reflect.Pointer {
|
||||
vvv = vvv.Elem()
|
||||
}
|
||||
if vvv.Kind() == reflect.Slice {
|
||||
typ := reflect.PointerTo(q.model.Type)
|
||||
slic := reflect.New(reflect.SliceOf(typ))
|
||||
for i := 0; i < vvv.Len(); i++ {
|
||||
val2 := vvv.Index(i).Interface()
|
||||
aid, docOk := val2.(IDocument)
|
||||
if docOk {
|
||||
rdoc := q.rawDoc.(bson.A)
|
||||
aid.setRaw(rdoc[i])
|
||||
aid.Populate(fields...)
|
||||
if reflect.ValueOf(aid).Kind() == reflect.Pointer {
|
||||
slic.Elem().Set(reflect.Append(slic.Elem(), reflect.ValueOf(aid)))
|
||||
} else {
|
||||
slic.Elem().Set(reflect.Append(slic, reflect.ValueOf(aid)))
|
||||
}
|
||||
}
|
||||
}
|
||||
q.doc = slic.Interface()
|
||||
} else if asDoc, ok2 := q.doc.(IDocument); ok2 {
|
||||
asDoc.setRaw(q.rawDoc)
|
||||
asDoc.Populate(fields...)
|
||||
}
|
||||
return q
|
||||
}
|
||||
func (q *Query) reOrganize() {
|
||||
var trvo reflect.Value
|
||||
if arr, ok := q.rawDoc.(bson.A); ok {
|
||||
typ := ModelRegistry[q.model.typeName].Type
|
||||
if typ.Kind() != reflect.Pointer {
|
||||
typ = reflect.PointerTo(typ)
|
||||
}
|
||||
slic := reflect.New(reflect.SliceOf(typ))
|
||||
for _, v2 := range arr {
|
||||
inter := reflect.ValueOf(rerere(v2, typ, false))
|
||||
slic.Elem().Set(reflect.Append(slic.Elem(), inter))
|
||||
}
|
||||
trvo = slic.Elem()
|
||||
} else {
|
||||
trvo = reflect.ValueOf(rerere(q.rawDoc, reflect.TypeOf(q.doc), false))
|
||||
}
|
||||
|
||||
resV := reflect.ValueOf(q.doc)
|
||||
for {
|
||||
if resV.Kind() == reflect.Pointer {
|
||||
if resV.Elem().Kind() == reflect.Slice {
|
||||
resV = resV.Elem()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if resV.CanSet() {
|
||||
resV.Set(trvo)
|
||||
} else {
|
||||
resV.Elem().Set(trvo.Elem())
|
||||
}
|
||||
}
|
||||
|
||||
func rerere(input interface{}, resType reflect.Type, isJson bool) interface{} {
|
||||
t := reflect.TypeOf(input)
|
||||
v := reflect.ValueOf(input)
|
||||
var key string
|
||||
if isJson {
|
||||
key = "json"
|
||||
} else {
|
||||
key = "bson"
|
||||
}
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
if v.Type().Kind() == reflect.Pointer {
|
||||
v = v.Elem()
|
||||
}
|
||||
if t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if resType.Kind() == reflect.Pointer {
|
||||
resType = resType.Elem()
|
||||
}
|
||||
var resV reflect.Value
|
||||
var newInstance interface{}
|
||||
if _, _, has := ModelRegistry.Has(reflect.New(resType).Elem().Interface()); has {
|
||||
newInstance = Create(reflect.New(resType).Elem().Interface())
|
||||
resV = reflect.ValueOf(newInstance)
|
||||
} else {
|
||||
newInstance = ModelRegistry.newForType(resType)
|
||||
if newInstance == nil && resType.Kind() == reflect.Pointer {
|
||||
newInstance = ModelRegistry.newForType(resType.Elem())
|
||||
if newInstance == nil {
|
||||
resV = reflect.New(resType)
|
||||
} else {
|
||||
resV = reflect.ValueOf(newInstance)
|
||||
}
|
||||
} else if newInstance != nil {
|
||||
resV = reflect.ValueOf(newInstance)
|
||||
} else {
|
||||
resV = reflect.New(resType)
|
||||
}
|
||||
}
|
||||
|
||||
var rve = resV
|
||||
if rve.Kind() == reflect.Pointer {
|
||||
rve = resV.Elem()
|
||||
}
|
||||
|
||||
if d, isD := v.Interface().(bson.D); isD {
|
||||
m := bson.M{}
|
||||
for _, el := range d {
|
||||
m[el.Key] = el.Value
|
||||
}
|
||||
input = m
|
||||
v = reflect.ValueOf(input)
|
||||
}
|
||||
|
||||
switch resType.Kind() {
|
||||
case reflect.Struct:
|
||||
shouldBreak := false
|
||||
mipmap, ok := v.Interface().(bson.M)
|
||||
if !ok {
|
||||
var omap map[string]interface{}
|
||||
if omap, ok = v.Interface().(map[string]interface{}); ok {
|
||||
mipmap = bson.M(omap)
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
for i := 0; i < resType.NumField(); i++ {
|
||||
ft := resType.Field(i)
|
||||
fv := rve.Field(i)
|
||||
if ft.Anonymous {
|
||||
fv.Set(handleAnon(input, ft.Type, fv))
|
||||
continue
|
||||
}
|
||||
tags, err := structtag.Parse(string(ft.Tag))
|
||||
panik(err)
|
||||
btag, err := tags.Get(key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if btag.Name == "-" {
|
||||
continue
|
||||
}
|
||||
intermediate := mipmap[btag.Name]
|
||||
_, err = tags.Get("ref")
|
||||
if err != nil {
|
||||
var tmp interface{}
|
||||
if ttmp, tok := intermediate.(bson.DateTime); tok {
|
||||
tmp = ttmp.Time()
|
||||
} else {
|
||||
tmp = rerere(intermediate, ft.Type, isJson)
|
||||
}
|
||||
fuck := reflect.ValueOf(tmp)
|
||||
if tmp != nil {
|
||||
if fuck.Type().Kind() == reflect.Pointer {
|
||||
fuck = fuck.Elem()
|
||||
}
|
||||
fv.Set(fuck)
|
||||
}
|
||||
shouldBreak = true
|
||||
} else {
|
||||
tt := ft.Type
|
||||
if tt.Kind() == reflect.Pointer {
|
||||
tt = tt.Elem()
|
||||
}
|
||||
tmp := rerere(intermediate, ft.Type, isJson)
|
||||
if tmp != nil {
|
||||
if reflect.ValueOf(tmp).Kind() == reflect.Pointer && fv.Kind() != reflect.Pointer {
|
||||
fv.Set(reflect.ValueOf(tmp).Elem())
|
||||
} else {
|
||||
fv.Set(reflect.ValueOf(tmp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if shouldBreak {
|
||||
// break
|
||||
}
|
||||
} else {
|
||||
nunu := ModelRegistry.new_(resType.Name())
|
||||
ider, ok := nunu.(HasID)
|
||||
if ok {
|
||||
toConvert := reflect.ValueOf(ider.Id()).Type()
|
||||
if v.Type() != toConvert {
|
||||
if v.CanConvert(toConvert) {
|
||||
ider.SetId(v.Convert(toConvert).Interface())
|
||||
}
|
||||
} else {
|
||||
ider.SetId(v.Interface())
|
||||
}
|
||||
if reflect.ValueOf(ider).Kind() == reflect.Pointer {
|
||||
nunu = reflect.ValueOf(ider).Elem().Interface()
|
||||
}
|
||||
rve.Set(reflect.ValueOf(nunu))
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
_, aOk := v.Interface().(bson.A)
|
||||
var arr []interface{}
|
||||
if !aOk {
|
||||
arr = v.Interface().([]interface{})
|
||||
} else {
|
||||
arr = []interface{}(v.Interface().(bson.A))
|
||||
}
|
||||
for _, it := range arr {
|
||||
if it != nil {
|
||||
tmp := reflect.ValueOf(rerere(it, rve.Type().Elem(), isJson))
|
||||
if tmp.Kind() == reflect.Pointer {
|
||||
tmp = tmp.Elem()
|
||||
}
|
||||
rve.Set(reflect.Append(rve, tmp))
|
||||
}
|
||||
}
|
||||
default:
|
||||
if resType.AssignableTo(v.Type()) {
|
||||
rve.Set(reflect.ValueOf(input))
|
||||
} else {
|
||||
switch rve.Interface().(type) {
|
||||
case int, int32, int64, uint, uint32, uint64:
|
||||
rve.Set(reflect.ValueOf(coerceInt(v, rve)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resV.Interface()
|
||||
}
|
||||
|
||||
func handleAnon(raw interface{}, rtype reflect.Type, rval reflect.Value) reflect.Value {
|
||||
f := rval
|
||||
g := rtype
|
||||
if rtype.Kind() == reflect.Pointer {
|
||||
g = rtype.Elem()
|
||||
}
|
||||
if !f.CanSet() {
|
||||
f = reflect.New(g)
|
||||
f.Elem().Set(rval)
|
||||
}
|
||||
if rtype.Kind() != reflect.Struct {
|
||||
return rval
|
||||
}
|
||||
|
||||
for i := 0; i < rtype.NumField(); i++ {
|
||||
typeField := rtype.Field(i)
|
||||
valueField := f.Field(i)
|
||||
tags, err := structtag.Parse(string(typeField.Tag))
|
||||
if !typeField.IsExported() {
|
||||
continue
|
||||
}
|
||||
if err == nil {
|
||||
var btag *structtag.Tag
|
||||
btag, err = tags.Get("bson")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if btag.Name == "-" {
|
||||
continue
|
||||
}
|
||||
amap, ok := raw.(bson.M)
|
||||
if ok {
|
||||
fval := amap[btag.Name]
|
||||
if reflect.TypeOf(fval) == reflect.TypeFor[bson.DateTime]() {
|
||||
fval = fval.(bson.DateTime).Time()
|
||||
}
|
||||
if valueField.Kind() == reflect.Pointer {
|
||||
valueField.Elem().Set(reflect.ValueOf(fval))
|
||||
} else {
|
||||
if reflect.TypeOf(fval) == reflect.TypeFor[string]() && typeField.Type == reflect.TypeFor[time.Time]() {
|
||||
tt, _ := time.Parse(time.RFC3339, fval.(string))
|
||||
valueField.Set(reflect.ValueOf(tt))
|
||||
} else if fval != nil {
|
||||
valueField.Set(reflect.ValueOf(fval))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// JSON - marshals this Query's results into json format
|
||||
func (q *Query) JSON() (string, error) {
|
||||
res, err := json.MarshalIndent(q.doc, "", "\t")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(res[:]), nil
|
||||
}
|
||||
|
||||
// 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 (q *Query) Exec(result interface{}) {
|
||||
if q.done {
|
||||
panic("Exec() has already been called!")
|
||||
}
|
||||
doc := reflect.ValueOf(q.doc)
|
||||
if doc.Elem().Kind() == reflect.Slice {
|
||||
for i := 0; i < doc.Elem().Len(); i++ {
|
||||
cur := doc.Elem().Index(i)
|
||||
imodel, ok := cur.Interface().(IDocument)
|
||||
if ok {
|
||||
imodel.setExists(true)
|
||||
imodel.SetSelf(imodel)
|
||||
doc.Elem().Index(i).Set(reflect.ValueOf(imodel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if idoc, ok := q.doc.(IDocument); ok {
|
||||
idoc.SetSelf(result)
|
||||
}
|
||||
if rdoc, ok2 := result.(IDocument); ok2 {
|
||||
rdoc.SetSelf(result)
|
||||
}
|
||||
reflect.ValueOf(result).Elem().Set(reflect.ValueOf(q.doc).Elem())
|
||||
q.done = true
|
||||
}
|
417
registry.go
Normal file
417
registry.go
Normal file
@ -0,0 +1,417 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
context2 "context"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/structtag"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
exists bool
|
||||
}
|
||||
|
||||
type gridFSReference struct {
|
||||
BucketName string
|
||||
FilenameFmt string
|
||||
LoadType reflect.Type
|
||||
Idx int
|
||||
}
|
||||
|
||||
type TModelRegistry map[string]*Model
|
||||
|
||||
// ModelRegistry - the ModelRegistry stores a map containing
|
||||
// pointers to Model instances, keyed by an associated
|
||||
// model name
|
||||
var ModelRegistry = make(TModelRegistry)
|
||||
|
||||
// DB - The mongodb database handle
|
||||
var DB *mongo.Database
|
||||
|
||||
// DBClient - The mongodb client
|
||||
var DBClient *mongo.Client
|
||||
|
||||
// NextStringID - Override this function with your own
|
||||
// string ID generator!
|
||||
var NextStringID func() string
|
||||
|
||||
var mutex sync.Mutex
|
||||
|
||||
func makeGfsRef(tag *structtag.Tag, idx int) gridFSReference {
|
||||
opts := tag.Options
|
||||
var ffmt string
|
||||
if len(opts) < 1 {
|
||||
ffmt = "%s"
|
||||
} else {
|
||||
ffmt = opts[0]
|
||||
}
|
||||
var typ reflect.Type
|
||||
if len(opts) < 2 {
|
||||
typ = reflect.TypeOf("")
|
||||
} else {
|
||||
switch opts[1] {
|
||||
case "bytes":
|
||||
typ = reflect.TypeOf([]byte{})
|
||||
case "string":
|
||||
typ = reflect.TypeOf("")
|
||||
default:
|
||||
typ = reflect.TypeOf("")
|
||||
}
|
||||
}
|
||||
|
||||
return gridFSReference{
|
||||
FilenameFmt: ffmt,
|
||||
BucketName: tag.Name,
|
||||
LoadType: typ,
|
||||
Idx: idx,
|
||||
}
|
||||
}
|
||||
|
||||
func makeRef(idx int, modelName string, fieldName string, ht reflect.Type) Reference {
|
||||
if modelName != "" {
|
||||
if ModelRegistry.Index(modelName) != -1 {
|
||||
return Reference{
|
||||
Idx: idx,
|
||||
Model: modelName,
|
||||
HydratedType: ht,
|
||||
Kind: ht.Kind(),
|
||||
exists: true,
|
||||
FieldName: fieldName,
|
||||
}
|
||||
}
|
||||
return Reference{
|
||||
Idx: idx,
|
||||
Model: modelName,
|
||||
FieldName: fieldName,
|
||||
HydratedType: ht,
|
||||
Kind: ht.Kind(),
|
||||
exists: true,
|
||||
}
|
||||
}
|
||||
panic("model name was empty")
|
||||
}
|
||||
|
||||
type parseResult []string
|
||||
|
||||
func (p parseResult) includes(str string) bool {
|
||||
for _, v := range p {
|
||||
if v == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseTags(t reflect.Type, v reflect.Value, lastParsed parseResult, depth int) (map[string][]InternalIndex, map[string]Reference, map[string]gridFSReference, string) {
|
||||
coll := ""
|
||||
refs := make(map[string]Reference)
|
||||
idcs := make(map[string][]InternalIndex)
|
||||
gfsRefs := make(map[string]gridFSReference)
|
||||
if depth >= 4 {
|
||||
return idcs, refs, gfsRefs, coll
|
||||
}
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
sft := t.Field(i)
|
||||
ft := sft.Type
|
||||
tags, err := structtag.Parse(string(sft.Tag))
|
||||
panik(err)
|
||||
switch ft.Kind() {
|
||||
case reflect.Slice:
|
||||
ft = ft.Elem()
|
||||
fallthrough
|
||||
case reflect.Pointer:
|
||||
if ft.Kind() == reflect.Pointer {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
fallthrough
|
||||
case reflect.Struct:
|
||||
if ft.ConvertibleTo(reflect.TypeOf(Document{})) {
|
||||
collTag, err := tags.Get("coll")
|
||||
panik(err)
|
||||
coll = collTag.Name
|
||||
idxTag, err := tags.Get("idx")
|
||||
if err == nil {
|
||||
idcs[sft.Type.Name()] = scanIndex(idxTag.Value())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if lastParsed.includes(sft.Name) {
|
||||
continue
|
||||
}
|
||||
blip := lastParsed
|
||||
blip = append(blip, sft.Name)
|
||||
if ft.Kind() == reflect.Struct && ft != reflect.TypeFor[time.Time]() {
|
||||
ii2, rr2, gg2, _ := parseTags(ft, reflect.New(ft).Elem(), blip, depth+1)
|
||||
for k, vv := range ii2 {
|
||||
idcs[sft.Name+"."+k] = vv
|
||||
}
|
||||
for k, vv := range rr2 {
|
||||
refs[sft.Name+"."+k] = vv
|
||||
}
|
||||
for k, vv := range gg2 {
|
||||
gfsRefs[sft.Name+"."+k] = vv
|
||||
}
|
||||
}
|
||||
if refTag, ok := tags.Get("ref"); ok == nil {
|
||||
sname := sft.Name
|
||||
refs[sname] = makeRef(i, refTag.Name, sft.Name, sft.Type)
|
||||
}
|
||||
if gtag, ok := tags.Get("gridfs"); ok == nil {
|
||||
sname := sft.Name + "@" + gtag.Name
|
||||
gfsRefs[sname] = makeGfsRef(gtag, i)
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
idxTag, err := tags.Get("idx")
|
||||
if err == nil {
|
||||
idcs[sft.Name] = scanIndex(idxTag.Value())
|
||||
}
|
||||
if gtag, ok := tags.Get("gridfs"); ok == nil {
|
||||
sname := sft.Name + "@" + gtag.Name
|
||||
gfsRefs[sname] = makeGfsRef(gtag, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return idcs, refs, gfsRefs, coll
|
||||
}
|
||||
|
||||
// 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 (r TModelRegistry) Has(i interface{}) (string, *Model, bool) {
|
||||
t := reflect.TypeOf(i)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
n := t.Name()
|
||||
if rT, ok := ModelRegistry[n]; ok {
|
||||
return n, rT, true
|
||||
}
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// HasByName functions almost identically to Has,
|
||||
// except that it takes a string as its argument.
|
||||
func (r TModelRegistry) HasByName(n string) (string, *Model, bool) {
|
||||
if t, ok := ModelRegistry[n]; ok {
|
||||
return n, t, true
|
||||
}
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// Index returns the index at which the Document struct is embedded
|
||||
func (r TModelRegistry) Index(n string) int {
|
||||
if v, ok := ModelRegistry[n]; ok {
|
||||
return v.idx
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (r TModelRegistry) new_(n string) interface{} {
|
||||
if name, m, ok := ModelRegistry.HasByName(n); ok {
|
||||
v := reflect.New(m.Type)
|
||||
av := reflect.New(reflect.PointerTo(m.Type)).Elem()
|
||||
av.Addr().Elem().Set(v)
|
||||
df := av.Addr().Elem().Elem().Field(m.idx)
|
||||
ado := reflect.New(reflect.PointerTo(df.Type())).Elem()
|
||||
do := reflect.New(df.Type())
|
||||
ado.Addr().Elem().Set(do)
|
||||
d := ado.Addr().Elem().Interface().(IDocument)
|
||||
d.newPopulationMap()
|
||||
//d := df.Interface().(IDocument)
|
||||
for k := range m.references {
|
||||
d.markDepopulated(k)
|
||||
}
|
||||
d.setModel(*m)
|
||||
d.getModel().typeName = name
|
||||
d.SetSelf(av.Interface())
|
||||
df.Set(reflect.ValueOf(d).Elem())
|
||||
return av.Interface()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r TModelRegistry) newForType(rt reflect.Type) interface{} {
|
||||
return r.new_(rt.Name())
|
||||
}
|
||||
|
||||
func (r TModelRegistry) Get(name string) *Model {
|
||||
model, ok := r[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
// Model registers models in the ModelRegistry, where
|
||||
// they can be accessed via a model's struct name
|
||||
func (r TModelRegistry) Model(mdl ...any) {
|
||||
defer mutex.Unlock()
|
||||
mutex.Lock()
|
||||
|
||||
for _, m := range mdl {
|
||||
t := reflect.TypeOf(m)
|
||||
v := reflect.ValueOf(m)
|
||||
vp := v
|
||||
if vp.Kind() != reflect.Ptr {
|
||||
vp = reflect.New(v.Type())
|
||||
vp.Elem().Set(v)
|
||||
}
|
||||
id, ok := vp.Interface().(HasID)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("you MUST implement the HasID interface!!! skipping...\n"))
|
||||
}
|
||||
switch (id).Id().(type) {
|
||||
case int, int64, int32, string, bson.ObjectID, uint, uint32, uint64:
|
||||
break
|
||||
default:
|
||||
log.Printf("invalid ID type specified!!! skipping...\n")
|
||||
}
|
||||
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = reflect.Indirect(reflect.ValueOf(m)).Type()
|
||||
v = reflect.ValueOf(m).Elem()
|
||||
}
|
||||
n := t.Name()
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("Only structs can be passed to this function, silly! (passed type: %s)", n))
|
||||
}
|
||||
idx := -1
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
ft := t.Field(i)
|
||||
if (ft.Type.ConvertibleTo(reflect.TypeOf(Document{}))) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
panic("A model must embed the Document struct!")
|
||||
}
|
||||
inds, refs, gfs, coll := parseTags(t, v, make(parseResult, 0), 0)
|
||||
if coll == "" {
|
||||
panic(fmt.Sprintf("a Document needs to be given a collection name! (passed type: %s)", n))
|
||||
}
|
||||
ModelRegistry[n] = &Model{
|
||||
idx: idx,
|
||||
Type: t,
|
||||
collection: coll,
|
||||
Indexes: inds,
|
||||
references: refs,
|
||||
gridFSReferences: gfs,
|
||||
typeName: n,
|
||||
}
|
||||
}
|
||||
for k, v := range ModelRegistry {
|
||||
for k2, v2 := range v.references {
|
||||
if !v2.exists {
|
||||
if _, ok := ModelRegistry[v2.FieldName]; ok {
|
||||
tmp := ModelRegistry[k].references[k2]
|
||||
ModelRegistry[k].references[k2] = Reference{
|
||||
Model: k,
|
||||
Idx: tmp.Idx,
|
||||
FieldName: tmp.FieldName,
|
||||
Kind: tmp.Kind,
|
||||
HydratedType: tmp.HydratedType,
|
||||
exists: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func innerWatch(coll *mongo.Collection) {
|
||||
sspipeline := mongo.Pipeline{
|
||||
bson.D{{"$match", bson.D{{"$or",
|
||||
bson.A{
|
||||
bson.D{{
|
||||
"operationType", "insert",
|
||||
}},
|
||||
bson.D{{"operationType", "update"}},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
stream, err := coll.Watch(context.TODO(), sspipeline, options.ChangeStream().SetFullDocument(options.UpdateLookup).SetFullDocumentBeforeChange(options.WhenAvailable))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func(stream *mongo.ChangeStream, ctx context2.Context) {
|
||||
err := stream.Close(ctx)
|
||||
panik(err)
|
||||
}(stream, context.TODO())
|
||||
for stream.Next(context.TODO()) {
|
||||
var data bson.M
|
||||
if err := stream.Decode(&data); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var uid any
|
||||
|
||||
docKey := data["documentKey"]
|
||||
|
||||
switch docKey.(type) {
|
||||
case bson.M:
|
||||
uid = docKey.(bson.M)["_id"]
|
||||
case bson.D:
|
||||
for _, vv := range docKey.(bson.D) {
|
||||
if vv.Key == "_id" {
|
||||
uid = vv.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//var uid = data["documentKey"].(bson.M)["_id"]
|
||||
|
||||
if data["operationType"] == "insert" {
|
||||
counterColl := DB.Collection(COUNTER_COL)
|
||||
counterColl.UpdateOne(context.TODO(), bson.M{"collection": coll.Name()}, bson.M{"$set": bson.M{
|
||||
"current": uid,
|
||||
}}, options.UpdateOne().SetUpsert(true))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func Connect(uri string, dbName string) {
|
||||
cli, err := mongo.Connect(options.Client().ApplyURI(uri))
|
||||
if err != nil {
|
||||
log.Fatal("failed to open database")
|
||||
}
|
||||
panik(err)
|
||||
DB = cli.Database(dbName)
|
||||
colls, err := DB.ListCollectionNames(context.TODO(), bson.M{"name": bson.M{"$ne": COUNTER_COL}}, options.ListCollections().SetNameOnly(true))
|
||||
|
||||
for _, c := range colls {
|
||||
if c == COUNTER_COL {
|
||||
continue
|
||||
}
|
||||
go innerWatch(DB.Collection(c))
|
||||
}
|
||||
|
||||
DBClient = cli
|
||||
}
|
85
testing.go
85
testing.go
@ -9,59 +9,62 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-loremipsum/loremipsum"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type chapter struct {
|
||||
ID bson.ObjectID `json:"_id"`
|
||||
Title string `json:"chapterTitle" form:"chapterTitle"`
|
||||
ChapterID int `json:"chapterID" autoinc:"chapters"`
|
||||
Index int `json:"index" form:"index"`
|
||||
Words int `json:"words"`
|
||||
Notes string `json:"notes" form:"notes"`
|
||||
Genre []string `json:"genre" form:"genre"`
|
||||
Bands []band `json:"bands" ref:"band,bands"`
|
||||
Characters []string `json:"characters" form:"characters"`
|
||||
Relationships [][]string `json:"relationships" form:"relationships"`
|
||||
Adult bool `json:"adult" form:"adult"`
|
||||
Summary string `json:"summary" form:"summary"`
|
||||
Hidden bool `json:"hidden" form:"hidden"`
|
||||
LoggedInOnly bool `json:"loggedInOnly" form:"loggedInOnly"`
|
||||
Posted time.Time `json:"datePosted"`
|
||||
FileName string `json:"fileName"`
|
||||
Text string `json:"text" gridfs:"story_text,/stories/{{.ChapterID}}.txt"`
|
||||
ID bson.ObjectID `bson:"_id" json:"_id"`
|
||||
Title string `bson:"chapterTitle" json:"chapterTitle" form:"chapterTitle"`
|
||||
ChapterID int `bson:"id" json:"chapterID" autoinc:"chapters"`
|
||||
Index int `bson:"index" json:"index" form:"index"`
|
||||
Words int `bson:"words" json:"words"`
|
||||
Notes string `bson:"notes" json:"notes" form:"notes"`
|
||||
Genre []string `bson:"genre" json:"genre" form:"genre"`
|
||||
Bands []band `json:"bands" bson:"bands" ref:"band,bands"`
|
||||
Characters []string `bson:"characters" json:"characters" form:"characters"`
|
||||
Relationships [][]string `bson:"relationships" json:"relationships" form:"relationships"`
|
||||
Adult bool `bson:"adult" json:"adult" form:"adult"`
|
||||
Summary string `bson:"summary" json:"summary" form:"summary"`
|
||||
Hidden bool `bson:"hidden" json:"hidden" form:"hidden"`
|
||||
LoggedInOnly bool `bson:"loggedInOnly" json:"loggedInOnly" form:"loggedInOnly"`
|
||||
Posted time.Time `bson:"datePosted,omitempty" json:"datePosted"`
|
||||
FileName string `json:"fileName" bson:"-"`
|
||||
Text string `json:"text" bson:"-" gridfs:"story_text,/stories/{{.ChapterID}}.txt"`
|
||||
}
|
||||
|
||||
type band struct {
|
||||
ID int64 `json:"_id"`
|
||||
Document `json:",inline" coll:"bands"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Locked bool `json:"locked" form:"locked"`
|
||||
Characters []string `json:"characters" form:"characters"`
|
||||
ID int64 `bson:"_id" json:"_id"`
|
||||
Document `bson:",inline" json:",inline" coll:"bands"`
|
||||
Name string `bson:"name" json:"name" form:"name"`
|
||||
Locked bool `bson:"locked" json:"locked" form:"locked"`
|
||||
Characters []string `bson:"characters" json:"characters" form:"characters"`
|
||||
}
|
||||
type user struct {
|
||||
ID int64 `json:"_id"`
|
||||
Document `json:",inline" coll:"users"`
|
||||
Username string `json:"username"`
|
||||
Favs []user `json:"favs" ref:"user"`
|
||||
ID int64 `bson:"_id" json:"_id"`
|
||||
Document `bson:",inline" json:",inline" coll:"users"`
|
||||
Username string `bson:"username" json:"username"`
|
||||
Favs []user `bson:"favs" json:"favs" ref:"user"`
|
||||
}
|
||||
type story struct {
|
||||
ID int64 `json:"_id"`
|
||||
Document `json:",inline" coll:"stories"`
|
||||
Title string `json:"title" form:"title"`
|
||||
Author *user `json:"author" ref:"user"`
|
||||
CoAuthor *user `json:"coAuthor" ref:"user"`
|
||||
Chapters []chapter `json:"chapters"`
|
||||
Recs int `json:"recs"`
|
||||
Favs int `json:"favs"`
|
||||
Views int `json:"views"`
|
||||
Completed bool `json:"completed" form:"completed"`
|
||||
Downloads int `json:"downloads"`
|
||||
ID int64 `bson:"_id" json:"_id"`
|
||||
Document `bson:",inline" json:",inline" coll:"stories"`
|
||||
Title string `bson:"title" json:"title" form:"title"`
|
||||
Author *user `bson:"author" json:"author" ref:"user"`
|
||||
CoAuthor *user `bson:"coAuthor" json:"coAuthor" ref:"user"`
|
||||
Chapters []chapter `bson:"chapters" json:"chapters"`
|
||||
Recs int `bson:"recs" json:"recs"`
|
||||
Favs int `bson:"favs" json:"favs"`
|
||||
Views int `bson:"views" json:"views"`
|
||||
Completed bool `bson:"completed" json:"completed" form:"completed"`
|
||||
Downloads int `bson:"downloads" json:"downloads"`
|
||||
}
|
||||
type somethingWithNestedChapters struct {
|
||||
ID int64 `json:"_id"`
|
||||
Document `json:",inline" coll:"nested_stuff"`
|
||||
Chapters []chapter `json:"chapters"`
|
||||
NestedText string `json:"text" gridfs:"nested_text,/nested/{{.ID}}.txt"`
|
||||
ID int64 `bson:"_id" json:"_id"`
|
||||
Document `bson:",inline" json:",inline" coll:"nested_stuff"`
|
||||
Chapters []chapter `bson:"chapters" json:"chapters"`
|
||||
NestedText string `json:"text" bson:"-" gridfs:"nested_text,/nested/{{.ID}}.txt"`
|
||||
}
|
||||
|
||||
func (s *somethingWithNestedChapters) Id() any {
|
||||
|
290
util.go
Normal file
290
util.go
Normal file
@ -0,0 +1,290 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func panik(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func nameOf(i interface{}) string {
|
||||
v := reflect.ValueOf(i)
|
||||
var n string
|
||||
switch v.Kind() {
|
||||
case reflect.Slice, reflect.Map:
|
||||
if v.Type().Elem().Kind() == reflect.Pointer {
|
||||
n = v.Type().Elem().Elem().Name()
|
||||
}
|
||||
case reflect.Pointer:
|
||||
n = nameOf(reflect.Indirect(v).Interface())
|
||||
default:
|
||||
n = v.Type().Name()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func valueOf(i interface{}) reflect.Value {
|
||||
v := reflect.ValueOf(i)
|
||||
if v.Type().Kind() == reflect.Pointer {
|
||||
v = valueOf(reflect.Indirect(v).Interface())
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func asId(i interface{}) HasID {
|
||||
v := reflect.ValueOf(i)
|
||||
var asHasId HasID
|
||||
var ok bool
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
asHasId, ok = v.Interface().(HasID)
|
||||
if ok {
|
||||
return asHasId
|
||||
}
|
||||
v = reflect.New(v.Type())
|
||||
v.Elem().Set(reflect.ValueOf(i))
|
||||
fallthrough
|
||||
case reflect.Pointer:
|
||||
asHasId, ok = v.Interface().(HasID)
|
||||
if ok {
|
||||
return asHasId
|
||||
} else {
|
||||
panic("value does not implemenet `HasId`!")
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return asHasId
|
||||
}
|
||||
|
||||
func coerceInt(input reflect.Value, dst reflect.Value) interface{} {
|
||||
if input.Type().Kind() == reflect.Pointer {
|
||||
input = input.Elem()
|
||||
}
|
||||
if dst.Type().Kind() == reflect.Pointer {
|
||||
dst = dst.Elem()
|
||||
}
|
||||
if input.Type().ConvertibleTo(dst.Type()) {
|
||||
return input.Convert(dst.Type()).Interface()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var arrRegex, _ = regexp.Compile(`\[(?P<index>\d+)]$`)
|
||||
|
||||
func getNested(field string, aValue reflect.Value) (*reflect.Type, *reflect.Value, error) {
|
||||
if strings.HasPrefix(field, ".") || strings.HasSuffix(field, ".") {
|
||||
return nil, nil, fmt.Errorf(errFmtMalformedField, field)
|
||||
}
|
||||
value := aValue
|
||||
if value.Kind() == reflect.Pointer {
|
||||
value = value.Elem()
|
||||
}
|
||||
aft := value.Type()
|
||||
dots := strings.Split(field, ".")
|
||||
if value.Kind() != reflect.Struct {
|
||||
if value.Kind() == reflect.Slice {
|
||||
st := reflect.MakeSlice(value.Type().Elem(), 0, 0)
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
cur := value.Index(i)
|
||||
if len(dots) > 1 {
|
||||
_, cv, _ := getNested(strings.Join(dots[1:], "."), cur.FieldByName(dots[0]))
|
||||
reflect.Append(st, *cv)
|
||||
//return getNested(, "."), fv)
|
||||
} else {
|
||||
reflect.Append(st, cur)
|
||||
}
|
||||
}
|
||||
typ := st.Type().Elem()
|
||||
return &typ, &st, nil
|
||||
}
|
||||
if len(dots) > 1 {
|
||||
return nil, nil, ErrNotSliceOrStruct
|
||||
} else {
|
||||
return &aft, &value, nil
|
||||
}
|
||||
}
|
||||
ref := value
|
||||
if ref.Kind() == reflect.Pointer {
|
||||
ref = ref.Elem()
|
||||
}
|
||||
var fv = ref.FieldByName(arrRegex.ReplaceAllString(dots[0], ""))
|
||||
if arrRegex.FindString(dots[0]) != "" && fv.Kind() == reflect.Slice {
|
||||
matches := arrRegex.FindStringSubmatch(dots[0])
|
||||
ridx, _ := strconv.Atoi(matches[0])
|
||||
idx := ridx
|
||||
fv = fv.Index(idx)
|
||||
}
|
||||
|
||||
ft, _ := ref.Type().FieldByName(arrRegex.ReplaceAllString(dots[0], ""))
|
||||
if len(dots) > 1 {
|
||||
return getNested(strings.Join(dots[1:], "."), fv)
|
||||
} else {
|
||||
return &ft.Type, &fv, nil
|
||||
}
|
||||
}
|
||||
func makeSettable(rval reflect.Value, value interface{}) reflect.Value {
|
||||
if !rval.CanSet() {
|
||||
nv := reflect.New(rval.Type())
|
||||
nv.Elem().Set(reflect.ValueOf(value))
|
||||
return nv
|
||||
}
|
||||
return rval
|
||||
}
|
||||
|
||||
func incrementInterface(t interface{}) interface{} {
|
||||
switch pt := t.(type) {
|
||||
case uint:
|
||||
t = pt + 1
|
||||
case uint32:
|
||||
t = pt + 1
|
||||
case uint64:
|
||||
t = pt + 1
|
||||
case int:
|
||||
t = pt + 1
|
||||
case int32:
|
||||
t = pt + 1
|
||||
case int64:
|
||||
t = pt + 1
|
||||
case string:
|
||||
t = NextStringID()
|
||||
case bson.ObjectID:
|
||||
t = bson.NewObjectID()
|
||||
default:
|
||||
panic(ErrUnsupportedID)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func isValidId(t interface{}) bool {
|
||||
switch t.(type) {
|
||||
case uint, uint32, uint64, int, int32, int64, string, bson.ObjectID:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isObject(t interface{}) bool {
|
||||
switch t.(type) {
|
||||
case bson.M, bson.D:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func traverseFields(f string, val interface{}) (ret interface{}, remaining string) {
|
||||
split := strings.Split(f, ".")
|
||||
rv := reflect.ValueOf(val)
|
||||
for {
|
||||
if rv.Kind() == reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
{
|
||||
asAny := make([]any, 0)
|
||||
for _, s := range split {
|
||||
asAny = append(asAny, s)
|
||||
}
|
||||
fmt.Println(asAny...)
|
||||
}
|
||||
if rv.Kind() == reflect.Slice {
|
||||
ret = rv.Interface()
|
||||
remaining = strings.Join(split[1:], ".")
|
||||
fmt.Println("returning?")
|
||||
return
|
||||
}
|
||||
structField := rv.FieldByName(split[0])
|
||||
if structField.IsValid() {
|
||||
fmt.Println(structField.Interface())
|
||||
if len(split) > 1 {
|
||||
if structField.Kind() == reflect.Slice {
|
||||
ret = structField
|
||||
remaining = strings.Join(split[1:], ".")
|
||||
return
|
||||
}
|
||||
ret, remaining = traverseFields(strings.Join(split[1:], "."), structField.Interface())
|
||||
fmt.Printf("remaining = %s\n", remaining)
|
||||
} else {
|
||||
ret = structField.Interface()
|
||||
remaining = ""
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func pull(s reflect.Value, idx int, typ reflect.Type) reflect.Value {
|
||||
retI := reflect.New(reflect.SliceOf(typ))
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
if i == idx {
|
||||
continue
|
||||
}
|
||||
retI.Elem().Set(reflect.Append(retI.Elem(), s.Index(i)))
|
||||
}
|
||||
return retI.Elem()
|
||||
}
|
||||
|
||||
func checkStruct(ref reflect.Value) error {
|
||||
if ref.Kind() == reflect.Slice {
|
||||
return ErrAppendMultipleDocuments
|
||||
}
|
||||
if ref.Kind() != reflect.Struct {
|
||||
return ErrNotAStruct
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSlice(ref reflect.Value) error {
|
||||
if ref.Kind() != reflect.Slice {
|
||||
return ErrNotASlice
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertSlice[In, Out any](in []In) []Out {
|
||||
out := make([]Out, 0)
|
||||
for _, i := range in {
|
||||
ii, ok := any(i).(Out)
|
||||
if ok {
|
||||
out = append(out, ii)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeSliceToDocumentSlice(in any) *DocumentSlice {
|
||||
ret := make(DocumentSlice, 0)
|
||||
val := reflect.ValueOf(in)
|
||||
if val.Kind() == reflect.Pointer {
|
||||
val = val.Elem()
|
||||
}
|
||||
if val.Kind() == reflect.Slice {
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
if idoc, ok := val.Index(i).Interface().(IDocument); ok {
|
||||
ret = append(ret, idoc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &ret
|
||||
}
|
||||
|
||||
func filterMap[k comparable, v any](input map[k]v, pred func(key k, val v) bool) map[k]v {
|
||||
ret := make(map[k]v)
|
||||
for k1, v1 := range input {
|
||||
if pred(k1, v1) {
|
||||
ret[k1] = v1
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user