package service import ( "encoding/json" "errors" "fmt" "sort" "strings" "browser.local/platform/domain" "browser.local/platform/repo" ) type pluginDataQueryResult struct { Rows []map[string]any `json:"rows"` } func (svc *CoreService) projectPluginDataJobResult(job domain.Job) error { if job.Capability != domain.JobCapabilityRemoteRunDBSQLiteQuery || job.State != domain.JobStateSucceeded { return nil } templateKey := strings.TrimSpace(job.ExecutionInput.Inputs["templateKey"]) if templateKey == "" { return nil } instance, err := svc.store.ServerInstances().Get(job.ServerInstanceID) if err != nil { return err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return err } var template domain.GameClientBridgeQueryTemplateDeclaration for _, candidate := range plugin.GameClientBridge.QueryTemplates { if candidate.Key == templateKey { template = candidate break } } if template.RowTarget == nil { return nil } var result pluginDataQueryResult if err := json.Unmarshal([]byte(job.ExecutionResult.Content), &result); err != nil { return validationError("declared query result is not valid JSON") } mutationsByKey := make(map[string]domain.PluginDataMutation, len(result.Rows)) for _, row := range result.Rows { value := make(map[string]any, len(template.RowTarget.ColumnMappings)) for destination, source := range template.RowTarget.ColumnMappings { value[destination] = row[source] } key, err := pluginDataRowKey(value, template.RowTarget.UpsertKeys) if err != nil { return err } if template.RowTarget.WriteMode == domain.PluginDataRowWriteModeMerge { existing, getErr := svc.store.PluginDataRecords().Get(pluginDataID(instance.ID, plugin.ID, template.RowTarget.Collection, key)) if getErr == nil { value = mergePluginDataValues(existing.Value, value) } else if !errors.Is(getErr, repo.ErrNotFound) { return getErr } } mutationsByKey[key] = domain.PluginDataMutation{Operation: domain.PluginDataMutationPut, Key: key, Value: value} } if template.RowTarget.WriteMode == domain.PluginDataRowWriteModeReplace { existing, listErr := svc.store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: template.RowTarget.Collection}) if listErr != nil { return listErr } for _, record := range existing { if _, present := mutationsByKey[record.Key]; !present { mutationsByKey[record.Key] = domain.PluginDataMutation{Operation: domain.PluginDataMutationDelete, Key: record.Key} } } } if len(mutationsByKey) == 0 { return nil } keys := make([]string, 0, len(mutationsByKey)) for key := range mutationsByKey { keys = append(keys, key) } sort.Strings(keys) mutations := make([]domain.PluginDataMutation, 0, len(keys)) for _, key := range keys { mutations = append(mutations, mutationsByKey[key]) } _, err = svc.applyPluginDataTransaction(domain.PluginDataTransaction{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: template.RowTarget.Collection, Mutations: mutations}) return err } func mergePluginDataValues(existing, incoming map[string]any) map[string]any { merged := domain.CopyGameClientBridgePayload(existing) if merged == nil { merged = make(map[string]any, len(incoming)) } for key, value := range incoming { merged[key] = value } return merged } func pluginDataRowKey(value map[string]any, keys []string) (string, error) { parts := make([]string, len(keys)) for index, key := range keys { item, exists := value[key] if !exists || item == nil || strings.TrimSpace(fmt.Sprint(item)) == "" { return "", validationError("declared query row is missing an upsert key") } encoded, err := json.Marshal(item) if err != nil { return "", validationError("declared query row upsert key is invalid") } parts[index] = string(encoded) } if len(parts) == 1 { var key string if err := json.Unmarshal([]byte(parts[0]), &key); err == nil { return key, nil } } return strings.Join(parts, "\x1f"), nil }