mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-02 12:39:50 +08:00
✨ feat(mongodb): 支持文档可视化编辑与删除
- 前端表格预览使用 _id 构建 MongoDB 行定位,并隐藏 typed ObjectID locator - 后端 ApplyChanges 支持 MongoDB 更新、单删和批量删除,区分 ObjectID 与字符串 _id - 补充 DataViewer、DataGrid 与双版本 Mongo driver 回归测试 Refs #458
This commit is contained in:
@@ -40,6 +40,7 @@ func (d *mongoProxyDialer) DialContext(ctx context.Context, network, address str
|
||||
}
|
||||
|
||||
const defaultMongoPort = 27017
|
||||
const mongoObjectIDLocatorColumn = "__gonavi_mongodb_id_locator__"
|
||||
|
||||
func normalizeMongoAddress(host string, port int) string {
|
||||
h := strings.TrimSpace(host)
|
||||
@@ -922,6 +923,7 @@ func (m *MongoDB) execFind(ctx context.Context, cmd bson.D) ([]map[string]interf
|
||||
var skip int64
|
||||
var sortDoc interface{}
|
||||
var projection interface{}
|
||||
var includeObjectIDLocator bool
|
||||
|
||||
for _, elem := range cmd {
|
||||
switch elem.Key {
|
||||
@@ -937,6 +939,10 @@ func (m *MongoDB) execFind(ctx context.Context, cmd bson.D) ([]map[string]interf
|
||||
sortDoc = elem.Value
|
||||
case "projection":
|
||||
projection = elem.Value
|
||||
case "__gonaviIncludeObjectIDLocator":
|
||||
if enabled, ok := elem.Value.(bool); ok {
|
||||
includeObjectIDLocator = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -978,6 +984,10 @@ func (m *MongoDB) execFind(ctx context.Context, cmd bson.D) ([]map[string]interf
|
||||
}
|
||||
row := make(map[string]interface{})
|
||||
for k, v := range doc {
|
||||
if includeObjectIDLocator && k == "_id" {
|
||||
row[mongoObjectIDLocatorColumn] = buildMongoObjectIDLocatorValue(v)
|
||||
columnSet[mongoObjectIDLocatorColumn] = true
|
||||
}
|
||||
row[k] = convertBsonValue(v)
|
||||
columnSet[k] = true
|
||||
}
|
||||
@@ -1006,6 +1016,13 @@ func (m *MongoDB) execFind(ctx context.Context, cmd bson.D) ([]map[string]interf
|
||||
return data, columns, nil
|
||||
}
|
||||
|
||||
func buildMongoObjectIDLocatorValue(v interface{}) interface{} {
|
||||
if oid, ok := v.(bson.ObjectID); ok {
|
||||
return bson.M{"$oid": oid.Hex()}
|
||||
}
|
||||
return convertBsonValue(v)
|
||||
}
|
||||
|
||||
// execCount 使用原生 Collection.CountDocuments() 执行计数
|
||||
func (m *MongoDB) execCount(ctx context.Context, cmd bson.D) ([]map[string]interface{}, []string, error) {
|
||||
var collName string
|
||||
@@ -1198,6 +1215,57 @@ func (m *MongoDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDef
|
||||
return []connection.TriggerDefinition{}, nil
|
||||
}
|
||||
|
||||
func copyMongoChangeDocument(row map[string]interface{}) bson.M {
|
||||
doc := bson.M{}
|
||||
for k, v := range row {
|
||||
doc[k] = v
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
func buildMongoChangeFilter(row map[string]interface{}) bson.M {
|
||||
filter := bson.M{}
|
||||
for k, v := range row {
|
||||
filter[k] = normalizeMongoChangeFilterValue(k, v)
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
func normalizeMongoChangeFilterValue(key string, value interface{}) interface{} {
|
||||
if strings.TrimSpace(key) != "_id" {
|
||||
return value
|
||||
}
|
||||
|
||||
switch val := value.(type) {
|
||||
case map[string]interface{}:
|
||||
if raw, ok := val["$oid"]; ok {
|
||||
if oid, parsed := parseMongoObjectIDHex(fmt.Sprintf("%v", raw)); parsed {
|
||||
return oid
|
||||
}
|
||||
}
|
||||
case bson.M:
|
||||
if raw, ok := val["$oid"]; ok {
|
||||
if oid, parsed := parseMongoObjectIDHex(fmt.Sprintf("%v", raw)); parsed {
|
||||
return oid
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func parseMongoObjectIDHex(value string) (bson.ObjectID, bool) {
|
||||
text := strings.TrimSpace(value)
|
||||
var zero bson.ObjectID
|
||||
if len(text) != 24 {
|
||||
return zero, false
|
||||
}
|
||||
oid, err := bson.ObjectIDFromHex(text)
|
||||
if err != nil {
|
||||
return zero, false
|
||||
}
|
||||
return oid, true
|
||||
}
|
||||
|
||||
// ApplyChanges implements batch changes for MongoDB
|
||||
func (m *MongoDB) ApplyChanges(tableName string, changes connection.ChangeSet) error {
|
||||
if m.client == nil {
|
||||
@@ -1211,23 +1279,21 @@ func (m *MongoDB) ApplyChanges(tableName string, changes connection.ChangeSet) e
|
||||
|
||||
// Process deletes
|
||||
for _, pk := range changes.Deletes {
|
||||
filter := bson.M{}
|
||||
for k, v := range pk {
|
||||
filter[k] = v
|
||||
}
|
||||
filter := buildMongoChangeFilter(pk)
|
||||
if len(filter) > 0 {
|
||||
if _, err := collection.DeleteOne(ctx, filter); err != nil {
|
||||
result, err := collection.DeleteOne(ctx, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除失败:%v", err)
|
||||
}
|
||||
if result.DeletedCount == 0 {
|
||||
return fmt.Errorf("删除失败:未匹配到文档")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process updates
|
||||
for _, update := range changes.Updates {
|
||||
filter := bson.M{}
|
||||
for k, v := range update.Keys {
|
||||
filter[k] = v
|
||||
}
|
||||
filter := buildMongoChangeFilter(update.Keys)
|
||||
if len(filter) == 0 {
|
||||
return fmt.Errorf("更新操作需要主键条件")
|
||||
}
|
||||
@@ -1237,17 +1303,18 @@ func (m *MongoDB) ApplyChanges(tableName string, changes connection.ChangeSet) e
|
||||
updateDoc["$set"].(bson.M)[k] = v
|
||||
}
|
||||
|
||||
if _, err := collection.UpdateOne(ctx, filter, updateDoc); err != nil {
|
||||
result, err := collection.UpdateOne(ctx, filter, updateDoc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新失败:%v", err)
|
||||
}
|
||||
if result.MatchedCount == 0 {
|
||||
return fmt.Errorf("更新失败:未匹配到文档")
|
||||
}
|
||||
}
|
||||
|
||||
// Process inserts
|
||||
for _, row := range changes.Inserts {
|
||||
doc := bson.M{}
|
||||
for k, v := range row {
|
||||
doc[k] = v
|
||||
}
|
||||
doc := copyMongoChangeDocument(row)
|
||||
if len(doc) > 0 {
|
||||
if _, err := collection.InsertOne(ctx, doc); err != nil {
|
||||
return fmt.Errorf("插入失败:%v", err)
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestApplyMongoURI_ExplicitHostDoesNotAdoptURIHosts(t *testing.T) {
|
||||
@@ -65,3 +67,64 @@ func TestMongoURI_MergesConnectionParamsIntoExistingURI(t *testing.T) {
|
||||
t.Fatalf("uri 未合并已有 URI query 与额外参数:%s", uri)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMongoChangeFilter_ConvertsExplicitOIDToObjectID(t *testing.T) {
|
||||
const oidHex = "507f1f77bcf86cd799439011"
|
||||
|
||||
filter := buildMongoChangeFilter(map[string]interface{}{
|
||||
"_id": map[string]interface{}{"$oid": oidHex},
|
||||
"name": oidHex,
|
||||
})
|
||||
|
||||
gotID, ok := filter["_id"].(bson.ObjectID)
|
||||
if !ok {
|
||||
t.Fatalf("expected _id to be bson.ObjectID, got %T", filter["_id"])
|
||||
}
|
||||
if gotID.Hex() != oidHex {
|
||||
t.Fatalf("unexpected ObjectID: got=%s want=%s", gotID.Hex(), oidHex)
|
||||
}
|
||||
if filter["name"] != oidHex {
|
||||
t.Fatalf("non-_id 24 hex string should stay string, got %T %v", filter["name"], filter["name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMongoChangeFilter_LeavesIDHexStringUntouched(t *testing.T) {
|
||||
const oidHex = "507f1f77bcf86cd799439011"
|
||||
|
||||
filter := buildMongoChangeFilter(map[string]interface{}{
|
||||
"_id": oidHex,
|
||||
})
|
||||
|
||||
if filter["_id"] != oidHex {
|
||||
t.Fatalf("plain _id string should stay string, got %T %v", filter["_id"], filter["_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMongoObjectIDLocatorValue_EncodesObjectID(t *testing.T) {
|
||||
const oidHex = "507f1f77bcf86cd799439011"
|
||||
oid, err := bson.ObjectIDFromHex(oidHex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
locator := buildMongoObjectIDLocatorValue(oid)
|
||||
locatorMap, ok := locator.(bson.M)
|
||||
if !ok {
|
||||
t.Fatalf("expected locator bson.M, got %T", locator)
|
||||
}
|
||||
if locatorMap["$oid"] != oidHex {
|
||||
t.Fatalf("unexpected locator value: %v", locatorMap["$oid"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyMongoChangeDocument_LeavesInsertIDStringUntouched(t *testing.T) {
|
||||
const oidHex = "507f1f77bcf86cd799439011"
|
||||
|
||||
doc := copyMongoChangeDocument(map[string]interface{}{
|
||||
"_id": oidHex,
|
||||
})
|
||||
|
||||
if doc["_id"] != oidHex {
|
||||
t.Fatalf("insert _id string should stay string, got %T %v", doc["_id"], doc["_id"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ func (d *mongoProxyDialer) DialContext(ctx context.Context, network, address str
|
||||
}
|
||||
|
||||
const defaultMongoPort = 27017
|
||||
const mongoObjectIDLocatorColumn = "__gonavi_mongodb_id_locator__"
|
||||
|
||||
func normalizeMongoAddress(host string, port int) string {
|
||||
h := strings.TrimSpace(host)
|
||||
@@ -925,6 +926,7 @@ func (m *MongoDBV1) execFind(ctx context.Context, cmd bson.D) ([]map[string]inte
|
||||
var skip int64
|
||||
var sortDoc interface{}
|
||||
var projection interface{}
|
||||
var includeObjectIDLocator bool
|
||||
|
||||
for _, elem := range cmd {
|
||||
switch elem.Key {
|
||||
@@ -940,6 +942,10 @@ func (m *MongoDBV1) execFind(ctx context.Context, cmd bson.D) ([]map[string]inte
|
||||
sortDoc = elem.Value
|
||||
case "projection":
|
||||
projection = elem.Value
|
||||
case "__gonaviIncludeObjectIDLocator":
|
||||
if enabled, ok := elem.Value.(bool); ok {
|
||||
includeObjectIDLocator = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -981,6 +987,10 @@ func (m *MongoDBV1) execFind(ctx context.Context, cmd bson.D) ([]map[string]inte
|
||||
}
|
||||
row := make(map[string]interface{})
|
||||
for k, v := range doc {
|
||||
if includeObjectIDLocator && k == "_id" {
|
||||
row[mongoObjectIDLocatorColumn] = buildMongoObjectIDLocatorValue(v)
|
||||
columnSet[mongoObjectIDLocatorColumn] = true
|
||||
}
|
||||
row[k] = convertBsonValue(v)
|
||||
columnSet[k] = true
|
||||
}
|
||||
@@ -1009,6 +1019,13 @@ func (m *MongoDBV1) execFind(ctx context.Context, cmd bson.D) ([]map[string]inte
|
||||
return data, columns, nil
|
||||
}
|
||||
|
||||
func buildMongoObjectIDLocatorValue(v interface{}) interface{} {
|
||||
if oid, ok := v.(primitive.ObjectID); ok {
|
||||
return bson.M{"$oid": oid.Hex()}
|
||||
}
|
||||
return convertBsonValue(v)
|
||||
}
|
||||
|
||||
// execCount 使用原生 Collection.CountDocuments() 执行计数
|
||||
func (m *MongoDBV1) execCount(ctx context.Context, cmd bson.D) ([]map[string]interface{}, []string, error) {
|
||||
var collName string
|
||||
@@ -1201,6 +1218,57 @@ func (m *MongoDBV1) GetTriggers(dbName, tableName string) ([]connection.TriggerD
|
||||
return []connection.TriggerDefinition{}, nil
|
||||
}
|
||||
|
||||
func copyMongoChangeDocument(row map[string]interface{}) bson.M {
|
||||
doc := bson.M{}
|
||||
for k, v := range row {
|
||||
doc[k] = v
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
func buildMongoChangeFilter(row map[string]interface{}) bson.M {
|
||||
filter := bson.M{}
|
||||
for k, v := range row {
|
||||
filter[k] = normalizeMongoChangeFilterValue(k, v)
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
func normalizeMongoChangeFilterValue(key string, value interface{}) interface{} {
|
||||
if strings.TrimSpace(key) != "_id" {
|
||||
return value
|
||||
}
|
||||
|
||||
switch val := value.(type) {
|
||||
case map[string]interface{}:
|
||||
if raw, ok := val["$oid"]; ok {
|
||||
if oid, parsed := parseMongoObjectIDHex(fmt.Sprintf("%v", raw)); parsed {
|
||||
return oid
|
||||
}
|
||||
}
|
||||
case bson.M:
|
||||
if raw, ok := val["$oid"]; ok {
|
||||
if oid, parsed := parseMongoObjectIDHex(fmt.Sprintf("%v", raw)); parsed {
|
||||
return oid
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func parseMongoObjectIDHex(value string) (primitive.ObjectID, bool) {
|
||||
text := strings.TrimSpace(value)
|
||||
var zero primitive.ObjectID
|
||||
if len(text) != 24 {
|
||||
return zero, false
|
||||
}
|
||||
oid, err := primitive.ObjectIDFromHex(text)
|
||||
if err != nil {
|
||||
return zero, false
|
||||
}
|
||||
return oid, true
|
||||
}
|
||||
|
||||
// ApplyChanges implements batch changes for MongoDB
|
||||
func (m *MongoDBV1) ApplyChanges(tableName string, changes connection.ChangeSet) error {
|
||||
if m.client == nil {
|
||||
@@ -1214,23 +1282,21 @@ func (m *MongoDBV1) ApplyChanges(tableName string, changes connection.ChangeSet)
|
||||
|
||||
// Process deletes
|
||||
for _, pk := range changes.Deletes {
|
||||
filter := bson.M{}
|
||||
for k, v := range pk {
|
||||
filter[k] = v
|
||||
}
|
||||
filter := buildMongoChangeFilter(pk)
|
||||
if len(filter) > 0 {
|
||||
if _, err := collection.DeleteOne(ctx, filter); err != nil {
|
||||
result, err := collection.DeleteOne(ctx, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除失败:%v", err)
|
||||
}
|
||||
if result.DeletedCount == 0 {
|
||||
return fmt.Errorf("删除失败:未匹配到文档")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process updates
|
||||
for _, update := range changes.Updates {
|
||||
filter := bson.M{}
|
||||
for k, v := range update.Keys {
|
||||
filter[k] = v
|
||||
}
|
||||
filter := buildMongoChangeFilter(update.Keys)
|
||||
if len(filter) == 0 {
|
||||
return fmt.Errorf("更新操作需要主键条件")
|
||||
}
|
||||
@@ -1240,17 +1306,18 @@ func (m *MongoDBV1) ApplyChanges(tableName string, changes connection.ChangeSet)
|
||||
updateDoc["$set"].(bson.M)[k] = v
|
||||
}
|
||||
|
||||
if _, err := collection.UpdateOne(ctx, filter, updateDoc); err != nil {
|
||||
result, err := collection.UpdateOne(ctx, filter, updateDoc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新失败:%v", err)
|
||||
}
|
||||
if result.MatchedCount == 0 {
|
||||
return fmt.Errorf("更新失败:未匹配到文档")
|
||||
}
|
||||
}
|
||||
|
||||
// Process inserts
|
||||
for _, row := range changes.Inserts {
|
||||
doc := bson.M{}
|
||||
for k, v := range row {
|
||||
doc[k] = v
|
||||
}
|
||||
doc := copyMongoChangeDocument(row)
|
||||
if len(doc) > 0 {
|
||||
if _, err := collection.InsertOne(ctx, doc); err != nil {
|
||||
return fmt.Errorf("插入失败:%v", err)
|
||||
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestApplyMongoURIV1_ExplicitHostDoesNotAdoptURIHosts(t *testing.T) {
|
||||
@@ -23,3 +26,64 @@ func TestApplyMongoURIV1_ExplicitHostDoesNotAdoptURIHosts(t *testing.T) {
|
||||
t.Fatalf("expected hosts to remain empty when explicit host exists, got %v", got.Hosts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMongoChangeFilterV1_ConvertsExplicitOIDToObjectID(t *testing.T) {
|
||||
const oidHex = "507f1f77bcf86cd799439011"
|
||||
|
||||
filter := buildMongoChangeFilter(map[string]interface{}{
|
||||
"_id": map[string]interface{}{"$oid": oidHex},
|
||||
"name": oidHex,
|
||||
})
|
||||
|
||||
gotID, ok := filter["_id"].(primitive.ObjectID)
|
||||
if !ok {
|
||||
t.Fatalf("expected _id to be primitive.ObjectID, got %T", filter["_id"])
|
||||
}
|
||||
if gotID.Hex() != oidHex {
|
||||
t.Fatalf("unexpected ObjectID: got=%s want=%s", gotID.Hex(), oidHex)
|
||||
}
|
||||
if filter["name"] != oidHex {
|
||||
t.Fatalf("non-_id 24 hex string should stay string, got %T %v", filter["name"], filter["name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMongoChangeFilterV1_LeavesIDHexStringUntouched(t *testing.T) {
|
||||
const oidHex = "507f1f77bcf86cd799439011"
|
||||
|
||||
filter := buildMongoChangeFilter(map[string]interface{}{
|
||||
"_id": oidHex,
|
||||
})
|
||||
|
||||
if filter["_id"] != oidHex {
|
||||
t.Fatalf("plain _id string should stay string, got %T %v", filter["_id"], filter["_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMongoObjectIDLocatorValueV1_EncodesObjectID(t *testing.T) {
|
||||
const oidHex = "507f1f77bcf86cd799439011"
|
||||
oid, err := primitive.ObjectIDFromHex(oidHex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
locator := buildMongoObjectIDLocatorValue(oid)
|
||||
locatorMap, ok := locator.(bson.M)
|
||||
if !ok {
|
||||
t.Fatalf("expected locator bson.M, got %T", locator)
|
||||
}
|
||||
if locatorMap["$oid"] != oidHex {
|
||||
t.Fatalf("unexpected locator value: %v", locatorMap["$oid"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyMongoChangeDocumentV1_LeavesInsertIDStringUntouched(t *testing.T) {
|
||||
const oidHex = "507f1f77bcf86cd799439011"
|
||||
|
||||
doc := copyMongoChangeDocument(map[string]interface{}{
|
||||
"_id": oidHex,
|
||||
})
|
||||
|
||||
if doc["_id"] != oidHex {
|
||||
t.Fatalf("insert _id string should stay string, got %T %v", doc["_id"], doc["_id"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user