diff --git a/.test/activity.json b/.test/activity.json new file mode 100644 index 0000000..f98769f --- /dev/null +++ b/.test/activity.json @@ -0,0 +1,56 @@ +{ + "_group": "system_collections", + "_name": "activity", + "_version": "202507", + "singular": "activity", + "plural": "activities", + "idPrefix": "act", + "system": true, + "indexSpecs": [{ + "name": "name_1", + "keys": { "name": 1 }, + "unique": true + }], + "schema": { + "type": "object", + "properties": { + "_id": { + "bsonType": "string" + }, + "person": { + "bsonType": "string" + }, + "name": { + "bsonType": "string" + }, + "createdAt": { + "bsonType": "date" + }, + "updatedAt": { + "bsonType": "date" + }, + "archivedAt": { + "bsonType": "date" + } + } + }, + "views": { + "activityExpanded": { + "viewOn": "activity", + "pipeline": [{ + "$lookup": { + "from": "person", + "localField": "person", + "foreignField": "_id", + "as": "refPerson" + } + }, { + "$unwind": "$refPerson" + }, { + "$set": { "person": "$refPerson" } + }, { + "$unset": "refPerson" + }] + } + } +} diff --git a/.test/person.json b/.test/person.json new file mode 100644 index 0000000..b08f48b --- /dev/null +++ b/.test/person.json @@ -0,0 +1,37 @@ +{ + "_group": "system_collections", + "_name": "person", + "_version": "202507", + "singular": "person", + "plural": "persons", + "idPrefix": "prs", + "system": true, + "indexSpecs": [{ + "name": "name_1", + "keys": { "name": 1 }, + "unique": true + }], + "schema": { + "type": "object", + "properties": { + "_id": { + "bsonType": "string" + }, + "name": { + "bsonType": "string" + }, + "age": { + "bsonType": "number" + }, + "createdAt": { + "bsonType": "date" + }, + "updatedAt": { + "bsonType": "date" + }, + "archivedAt": { + "bsonType": "date" + } + } + } +} diff --git a/.test/sample.json b/.test/sample.json new file mode 100644 index 0000000..d185afc --- /dev/null +++ b/.test/sample.json @@ -0,0 +1,20 @@ +[{ + "name": "PKCELL Ultra Alkaline Batteries Size D LR20-2B (2-Pack)", + "sku": "ALK-PK-LR202B-01", + "price": { + "value": 288, + "currency": "cad" + }, + "active": true, + "tags": ["alkaline", "size-d", "pkcell"] +}, { + "name": "Shimano Cleats SH-11 Yellow 6 degrees", + "sku": "BK-SM-SH51", + "active": true, + "tags": ["shimano", "cleats", "sh11", "cycling"] +}, { + "name": "OSRAM Light Bulbs H7 Original Classic 12V 55W Spare Part Replacement", + "sku": "LT-OSR-H712V35W", + "active": false, + "tags": ["osram", "h7", "halogen-bulb", "12v", "55w"] +}] \ No newline at end of file diff --git a/.test/user.json b/.test/user.json new file mode 100644 index 0000000..02174c7 --- /dev/null +++ b/.test/user.json @@ -0,0 +1,53 @@ +{ + "_group": "system_collections", + "_name": "user", + "_version": "202507", + "singular": "user", + "plural": "users", + "idPrefix": "usr", + "system": true, + "indexSpecs": [{ + "name": "username_1", + "keys": { "username": 1 }, + "unique": true + }, { + "name": "email_1", + "keys": { "email": 1 }, + "unique": true + }], + "schema": { + "type": "object", + "properties": { + "_id": { + "bsonType": "string" + }, + "fullname": { + "bsonType": "string" + }, + "firstname": { + "bsonType": "string" + }, + "username": { + "bsonType": "string" + }, + "password": { + "bsonType": "string" + }, + "email": { + "bsonType": "string" + }, + "credentials": { + "bsonType": ["object", "null"] + }, + "createdAt": { + "bsonType": "date" + }, + "updatedAt": { + "bsonType": "date" + }, + "archivedAt": { + "bsonType": "date" + } + } + } +} diff --git a/main.go b/main.go index 6ba5c1e..50cca1f 100644 --- a/main.go +++ b/main.go @@ -39,12 +39,13 @@ type IMongoClient interface { } type CollectionDefinition struct { - Name string `bson:"_name"` - Singular string `bson:"singular"` - Plural string `bson: "plural"` - IdPrefix string `bson: "idPrefix"` - IndexSpecs []map[string]any `bson: "indexSpecs"` - Schema map[string]any `bson: "schema"` + Name string `bson:"_name"` + Singular string `bson:"singular"` + Plural string `bson:"plural"` + IdPrefix string `bson:"idPrefix"` + IndexSpecs []map[string]any `bson:"indexSpecs"` + Schema map[string]any `bson:"schema"` + Views map[string]any `bson:"views"` } // func (cd *CollectionDefinition) GetSchema(name string) @@ -168,6 +169,8 @@ func (c *MongoClient) GetCollection(database, name string) *mongo.Collection { c.CreateIndexes(collection, cdef) + c.CreateViews(db, cdef) + return collection } } diff --git a/main_index.go b/main_index.go index 2eb9c9e..6454fb7 100644 --- a/main_index.go +++ b/main_index.go @@ -21,7 +21,7 @@ func (c *MongoClient) CreateIndexes(collection *mongo.Collection, cdef *Collecti indexModels := make([]mongo.IndexModel, 0) for _, keyDef := range cdef.IndexSpecs { - log.Printf("Key Definition %s", keyDef["name"]) + log.Printf("Key Definition [%s]%s", cdef.Name, keyDef["name"]) kdb, err := bson.Marshal(keyDef) if err != nil { diff --git a/main_views.go b/main_views.go new file mode 100644 index 0000000..85dd03e --- /dev/null +++ b/main_views.go @@ -0,0 +1,91 @@ +package mongo + +import ( + "context" + "log" + + // "runtime" + + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateViews will create views for the given collection and definition. +func (c *MongoClient) CreateViews(db *mongo.Database, cdef *CollectionDefinition) { + if cdef == nil || cdef.Views == nil { + log.Printf("No definitions will not create views") + + return + } + + // runtime.Breakpoint() + + for name, defVal := range cdef.Views { + + // 1. Decode definition + v, err := bson.Marshal(defVal) + if err != nil { + log.Printf("failed to marshal %v", err) + + continue + } + + // 2. Take the raw representation + vRaw := bson.Raw(v) + if err := vRaw.Validate(); err != nil { + log.Printf("failed to validate bson raw: %v", err) + + continue + } + + pipelineVal := vRaw.Lookup("pipeline") + pipelineArr, ok := pipelineVal.ArrayOK() + if !ok { + log.Printf("Unable to extract pipeline") + + continue + } + + pipeline := mongo.Pipeline{} + + successPipeline := true + pipelineValues, _ := pipelineArr.Values() + for _, o := range pipelineValues { + var stage bson.D + if err := bson.Unmarshal(o.Value, &stage); err != nil { + log.Printf("failed to unmarshal stage %v, %v", o, err) + + successPipeline = false + break + } + + pipeline = append(pipeline, stage) + } + + if successPipeline == false { + continue + } + + // Specify the Collation option to set a default collation for the view. + opts := options.CreateView().SetCollation(&options.Collation{ + Locale: "en_US", + }) + + viewonVal := vRaw.Lookup("viewOn") + viewOn, ok := viewonVal.StringValueOK(); + if !ok { + log.Printf("failed to find viewOn") + + continue + } + + err = db.CreateView(context.TODO(), name, viewOn, pipeline, opts) + if err != nil { + log.Printf("failed to create view %v", err) + + continue + } + } + +} \ No newline at end of file diff --git a/main_views_test.go b/main_views_test.go new file mode 100644 index 0000000..b5b1079 --- /dev/null +++ b/main_views_test.go @@ -0,0 +1,61 @@ +package mongo + +import ( + "os" + "context" + "strings" + "encoding/json" + "testing" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +func TestCreateViews(t *testing.T) { + // 1. Register schemas + schemaPerson, err := os.ReadFile("./.test/person.json") + if err != nil { t.Fatal(err) } + + schemaActivity, err := os.ReadFile("./.test/activity.json") + if err != nil { t.Fatal(err) } + + var person bson.M + if err := json.Unmarshal(schemaPerson, &person); err != nil { + t.Fatalf("Length: %d, First bytes: %x\n", len(schemaPerson), schemaPerson[:4]) + } + + var activity bson.M + if err := json.Unmarshal(schemaActivity, &activity); err != nil { + t.Fatalf("Length: %d, First bytes: %x\n", len(schemaActivity), schemaActivity[:4]) + } + + client := GetMongoClient() + client.AddDefinition(person) + client.AddDefinition(activity) + + // 2. Insert data + p1 := map[string]any { + "name": "MyName112", + "age": int32(25), + } + + o, err := client.InsertOne(context.Background(), "mydb", "person", p1) + if err != nil { t.Fatalf("Failed to insertOne %#v", err) } + + a1 := map[string]any { + "name": "Main activity", + "person": o["_id"], + } + + o, err = client.InsertOne(context.Background(), "mydb", "activity", a1) + if err != nil { t.Fatalf("Failed to insertOne %#v", err) } + + // 3. Should have activityExpanded defined, let's query it. + var results bson.M + filter := map[string]any { "person.name": "MyName112" } + c := client.Client.Database("mydb").Collection("activityExpanded") + c.FindOne(context.Background(), filter).Decode(&results) + + if !strings.HasPrefix(results["_id"].(string), "act_") { + t.Fatal("_id should have been prefixed") + } +} \ No newline at end of file