Hasura 中间件API go操作示例

Hasura 无后端数据库应用:零代码即刻上手 阅读详情

功能概述

这是一个用 Go 语言编写的 Hasura GraphQL 引擎管理工具,主要功能包括:

  • 自动发现和追踪数据库中的表和视图
  • 自动发现和追踪表之间的外键关系

3. 自动配置基础的访问权限

4. 提供数据库结构的统计信息

核心功能模块

1. 连接管理

  • TestHasuraConnection() - 测试 Hasura 连接是否正常
  • GetAllSources() - 获取所有配置的数据源

2. Schema 管理

  • GetDatabaseSchemas() - 获取数据库所有 schema 信息
  • GetDatabaseSources() - 获取数据源配置信息

3. 表和视图追踪

  • TrackUnTrackedObjects() - 自动追踪未追踪的表和视图
  • TrackTableOrView() - 追踪单个表或视图
  • GetUnTrackedTablesAndViews() - 获取未追踪的表和视图列表

4. 关系管理

  • TrackUnTrackedRelationships() - 自动追踪未追踪的外键关系
  • TrackRelationship() - 追踪单个外键关系

技术特点

  • 自动化管理
  • 自动发现数据库对象
  • 自动配置追踪关系
  • 自动设置基础权限
  • 完善的错误处理
  • 详细的错误信息
  • 失败处理机制
  • 统计信息输出
  • 模块化设计
  • 功能模块清晰分离
  • 代码复用性高
  • 易于维护和扩展
  • 友好的输出
  • 详细的进度信息
  • 清晰的统计数据
  • 格式化的结果展示

应用场景

1. 数据库初始化配置

  • 数据库结构变更后的同步
  • 新增数据源的快速接入
  • 数据库结构审计

使用建议

  • 首次配置 Hasura 时使用
  • 数据库结构发生变更后运行
  • 定期运行以确保配置同步
  • 用于检查数据库结构完整性

这个工具极大地简化了 Hasura GraphQL 引擎的配置过程,特别适合处理大型数据库的初始化配置和后续维护工作。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"
)

const (
	// 从环境变量获取 Hasura 配置
	hasuraEndpoint   = "http://127.0.0.1:8080/v1/graphql"  // 更新为实际的 Hasura 端点
	metadataEndpoint = "http://127.0.0.1:8080/v1/metadata" // 更新为实际的 metadata 端点
	adminSecret      = "HEllo"                        // 更新为实际的 admin secret
)

// GraphQLRequest represents a GraphQL query request
type GraphQLRequest struct {
	Query     string                 `json:"query"`
	Variables map[string]interface{} `json:"variables,omitempty"`
}

// TestHasuraConnection tests the connection to Hasura
func TestHasuraConnection() error {
	// Simple health check query
	query := `
		query HealthCheck {
			__schema {
				__typename
			}
		}
	`

	// Create request body
	reqBody := GraphQLRequest{
		Query: query,
	}
	jsonBody, err := json.Marshal(reqBody)
	if err != nil {
		return fmt.Errorf("请求体编码失败: %v", err)
	}

	// Create HTTP request
	req, err := http.NewRequest("POST", hasuraEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("创建请求失败: %v", err)
	}

	// Add headers
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	// Send request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// Read response
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("读取响应失败: %v", err)
	}

	// Check response status
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("意外的状态码: %d, 响应内容: %s", resp.StatusCode, string(body))
	}

	fmt.Printf("连接成功! 响应: %s\n", string(body))
	return nil
}

// TestQueryUsers tests querying users from Hasura
func TestQueryUsers() error {
	query := `
		query MyQuery {
  role(where: {id: {_eq: 34}}) {
    id
    role_name
  }
}
	`

	reqBody := GraphQLRequest{
		Query: query,
	}
	jsonBody, err := json.Marshal(reqBody)
	if err != nil {
		return fmt.Errorf("marshal request failed: %v", err)
	}

	req, err := http.NewRequest("POST", hasuraEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("create request failed: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %v", err)
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("read response failed: %v", err)
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
	}

	fmt.Printf("查询用户成功! 响应: %s\n", string(body))
	return nil
}

// HasuraMetadata 结构体用于解析 Hasura 元数据
type HasuraMetadata struct {
	Sources []struct {
		Name          string `json:"name"`
		Configuration struct {
			ConnectionInfo struct {
				DatabaseURL string `json:"database_url"`
			} `json:"connection_info"`
		} `json:"configuration"`
	} `json:"sources"`
}

func GetDatabaseSources() error {
	reqBody := map[string]interface{}{
		"type":    "export_metadata",
		"args":    map[string]interface{}{},
		"version": 2,
	}

	jsonBody, err := json.Marshal(reqBody)
	if err != nil {
		return fmt.Errorf("请求编码失败: %v", err)
	}

	req, err := http.NewRequest("POST", metadataEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("创建请求失败: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("read response failed: %v", err)
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
	}

	var result map[string]interface{}
	if err := json.Unmarshal(body, &result); err != nil {
		return fmt.Errorf("parse metadata failed: %v", err)
	}

	// 获取 sources 数组
	sources, ok := result["metadata"].(map[string]interface{})["sources"].([]interface{})
	if !ok {
		return fmt.Errorf("在元数据中未找到数据源")
	}

	fmt.Println("\n数据库连接:")
	fmt.Println("--------------------")
	for _, source := range sources {
		if sourceMap, ok := source.(map[string]interface{}); ok {
			if name, ok := sourceMap["name"].(string); ok {
				fmt.Printf("Name: %s\n", name)
				fmt.Println("--------------------")
			}
		}
	}

	return nil
}

// TrackTableOrView 追踪单个表或视图
func TrackTableOrView(sourceName, schema, objectName, objectType string) error {
	// 构建 track 请求
	trackQuery := map[string]interface{}{
		"type": "pg_track_table",  // 修改为正确的 API 端点
		"args": map[string]interface{}{
			"source": sourceName,
			"table": map[string]interface{}{
				"schema": schema,
				"name": objectName,
			},
			"configuration": map[string]interface{}{
				"custom_root_fields": map[string]interface{}{
					"select": fmt.Sprintf("select_%s", objectName),
					"select_by_pk": fmt.Sprintf("select_%s_by_pk", objectName),
					"select_aggregate": fmt.Sprintf("select_%s_aggregate", objectName),
					"insert": fmt.Sprintf("insert_%s", objectName),
					"insert_one": fmt.Sprintf("insert_%s_one", objectName),
					"update": fmt.Sprintf("update_%s", objectName),
					"update_by_pk": fmt.Sprintf("update_%s_by_pk", objectName),
					"delete": fmt.Sprintf("delete_%s", objectName),
					"delete_by_pk": fmt.Sprintf("delete_%s_by_pk", objectName),
				},
			},
		},
	}

	jsonBody, err := json.Marshal(trackQuery)
	if err != nil {
		return fmt.Errorf("追踪请求编码失败: %v", err)
	}

	// 打���请求内容以便调试
	fmt.Printf("追踪 %s.%s 的请求:\n%s\n", schema, objectName, string(jsonBody))

	req, err := http.NewRequest("POST", metadataEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("创建追踪请求失败: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("追踪请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 读取并打印响应内容
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("read response failed: %v", err)
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("追踪失败,状态码 %d: %s", resp.StatusCode, string(body))
	}

	fmt.Printf("追踪响应: %s\n", string(body))

	// 如果是视图,可能需要额外的配置
	if objectType == "VIEW" {
		// 为视图添加额外的配置
		configQuery := map[string]interface{}{
			"type": "pg_set_table_customization",
			"args": map[string]interface{}{
				"source": sourceName,
				"table": map[string]interface{}{
					"schema": schema,
					"name": objectName,
				},
				"configuration": map[string]interface{}{
					"identifier": objectName,
					"custom_name": objectName,
				},
			},
		}

		jsonBody, err = json.Marshal(configQuery)
		if err != nil {
			return fmt.Errorf("marshal view config request failed: %v", err)
		}

		req, err = http.NewRequest("POST", metadataEndpoint, bytes.NewBuffer(jsonBody))
		if err != nil {
			return fmt.Errorf("create view config request failed: %v", err)
		}

		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

		resp, err = client.Do(req)
		if err != nil {
			return fmt.Errorf("view config request failed: %v", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			body, _ := ioutil.ReadAll(resp.Body)
			return fmt.Errorf("view config failed with status %d: %s", resp.StatusCode, string(body))
		}
	}

	// 添加权限(可选)
	permissionQuery := map[string]interface{}{
		"type": "pg_create_select_permission",
		"args": map[string]interface{}{
			"source": sourceName,
			"table": map[string]interface{}{
				"schema": schema,
				"name": objectName,
			},
			"role": "user",
			"permission": map[string]interface{}{
				"columns": "*",
				"filter": map[string]interface{}{},
				"limit": 100,
			},
		},
	}

	jsonBody, err = json.Marshal(permissionQuery)
	if err != nil {
		return fmt.Errorf("marshal permission request failed: %v", err)
	}

	req, err = http.NewRequest("POST", metadataEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("create permission request failed: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	resp, err = client.Do(req)
	if err != nil {
		return fmt.Errorf("permission request failed: %v", err)
	}
	defer resp.Body.Close()

	// 不要因为权限设置失败而中断整个过程
	if resp.StatusCode != http.StatusOK {
		body, _ := ioutil.ReadAll(resp.Body)
		fmt.Printf("Warning: Failed to set permissions for %s.%s: %s\n", schema, objectName, string(body))
	}

	return nil
}

// TrackUnTrackedObjects 追踪未追踪的表和视图
func TrackUnTrackedObjects(sourceName string) error {
	// 1. 获取所有表和视图
	objectsQuery := map[string]interface{}{
		"type": "run_sql",
		"args": map[string]interface{}{
			"source": sourceName,
			"sql": `
				SELECT 
					table_schema,
					
					table_name,
					table_type
				FROM information_schema.tables 
				WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
				AND table_type IN ('BASE TABLE', 'VIEW')
				ORDER BY table_schema, table_type, table_name;
			`,
		},
	}

	dataEndpoint := strings.Replace(metadataEndpoint, "/v1/metadata", "/v2/query", 1)
	jsonBody, err := json.Marshal(objectsQuery)
	if err != nil {
		return fmt.Errorf("marshal objects request failed: %v", err)
	}

	req, err := http.NewRequest("POST", dataEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("create objects request failed: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("objects request failed: %v", err)
	}
	defer resp.Body.Close()

	var result struct {
		Result [][]string `json:"result"`
	}
	if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return fmt.Errorf("decode objects response failed: %v", err)
	}

	// 2. 获取已追踪的对象
	trackedQuery := map[string]interface{}{
		"type": "export_metadata",
		"args": map[string]interface{}{},
		"version": 2,
	}

	jsonBody, err = json.Marshal(trackedQuery)
	if err != nil {
		return fmt.Errorf("marshal tracked request failed: %v", err)
	}

	req, err = http.NewRequest("POST", metadataEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("create tracked request failed: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	resp, err = client.Do(req)
	if err != nil {
		return fmt.Errorf("tracked request failed: %v", err)
	}
	defer resp.Body.Close()

	var trackedResult map[string]interface{}
	if err = json.NewDecoder(resp.Body).Decode(&trackedResult); err != nil {
		return fmt.Errorf("decode tracked response failed: %v", err)
	}

	fmt.Printf("\n正在追踪数据源 '%s' 中未追踪的对象:\n", sourceName)
	fmt.Println("--------------------")

	stats := struct {
		TrackedTables   int
		TrackedViews    int
		UntrackedTables int
		UntrackedViews  int
	}{}

	// 3. 追踪未追踪的对象
	if len(result.Result) > 1 { // 跳过标题行
		for _, row := range result.Result[1:] {
			schema := row[0]
			objectName := row[1]
			objectType := row[2]

			// 检查对象是否已被追踪
			isTracked := false
			if sources, ok := trackedResult["metadata"].(map[string]interface{})["sources"].([]interface{}); ok {
				for _, source := range sources {
					if sourceMap, ok := source.(map[string]interface{}); ok {
						if sourceTables, ok := sourceMap["tables"].([]interface{}); ok {
							for _, sourceTable := range sourceTables {
								if tableMap, ok := sourceTable.(map[string]interface{}); ok {
									if tableInfo, ok := tableMap["table"].(map[string]interface{}); ok {
										if tableInfo["name"] == objectName && tableInfo["schema"] == schema {
											isTracked = true
											if objectType == "BASE TABLE" {
												stats.TrackedTables++
											} else {
												stats.TrackedViews++
											}
											break
										}
									}
								}
							}
						}
					}
				}
			}

			if !isTracked {
				fmt.Printf("追踪新的 %s: %s.%s\n", strings.ToLower(objectType), schema, objectName)
				
				if err := TrackTableOrView(sourceName, schema, objectName, objectType); err != nil {
					fmt.Printf("无法追踪 %s %s.%s: %v\n", strings.ToLower(objectType), schema, objectName, err)
					continue
				}

				if objectType == "BASE TABLE" {
					stats.UntrackedTables++
				} else {
					stats.UntrackedViews++
				}
			}
		}
	}

	fmt.Println("--------------------")
	fmt.Println("\n统计信息:")
	fmt.Printf("表格:\n")
	fmt.Printf("  - 已追踪: %d\n", stats.TrackedTables)
	fmt.Printf("  - 新追踪: %d\n", stats.UntrackedTables)
	fmt.Printf("  - 总计: %d\n", stats.TrackedTables+stats.UntrackedTables)
	fmt.Printf("视图:\n")
	fmt.Printf("  - 已追踪: %d\n", stats.TrackedViews)
	fmt.Printf("  - 新追踪: %d\n", stats.UntrackedViews)
	fmt.Printf("  - 总计: %d\n", stats.TrackedViews+stats.UntrackedViews)
	fmt.Println("--------------------")
	
	return nil
}

// GetDatabaseSchemas 获取所有数据库的 schema 信息
func GetDatabaseSchemas(sourceName string) error {
	// 构建获取 schema 的查询
	schemaQuery := map[string]interface{}{
		"type": "run_sql",
		"args": map[string]interface{}{
			"source": sourceName,
			"sql": `
				SELECT 
					schema_name 
				FROM information_schema.schemata 
				WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
				ORDER BY schema_name;
			`,
		},
	}

	jsonBody, err := json.Marshal(schemaQuery)
	if err != nil {
		return fmt.Errorf("marshal schema request failed: %v", err)
	}

	// 发送请求到 data endpoint
	dataEndpoint := strings.Replace(metadataEndpoint, "/v1/metadata", "/v2/query", 1)
	req, err := http.NewRequest("POST", dataEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("create schema request failed: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("schema request failed: %v", err)
	}
	defer resp.Body.Close()

	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return fmt.Errorf("decode schema response failed: %v", err)
	}

	// 输出 schema 信息
	fmt.Printf("\n数据源 '%s' 中的 Schema:\n", sourceName)
	fmt.Println("--------------------")

	if result["result"] != nil {
		rows, ok := result["result"].([]interface{})
		if !ok || len(rows) < 2 { // 第一行是列名
			return fmt.Errorf("unexpected schema result format")
		}

		// 跳过第一行(列名),显示所有 schema
		for _, row := range rows[1:] {
			if schemaRow, ok := row.([]interface{}); ok && len(schemaRow) > 0 {
				if schemaName, ok := schemaRow[0].(string); ok {
					fmt.Printf("Schema: %s\n", schemaName)
				}
			}
		}
	}

	fmt.Println("--------------------")
	return nil
}

// GetUnTrackedTablesAndViews 获取未追踪的表和视图
func GetUnTrackedTablesAndViews(sourceName string) error {
	// 1. 获取已追踪的对象
	trackedQuery := map[string]interface{}{
		"type":    "export_metadata",
		"args":    map[string]interface{}{},
		"version": 2,
	}

	jsonBody, err := json.Marshal(trackedQuery)
	if err != nil {
		return fmt.Errorf("marshal tracked request failed: %v", err)
	}

	req, err := http.NewRequest("POST", metadataEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("create tracked request failed: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("tracked request failed: %v", err)
	}
	defer resp.Body.Close()

	// 2. 获取所有表和视图
	allObjectsQuery := map[string]interface{}{
		"type": "run_sql",
		"args": map[string]interface{}{
			"source": sourceName,
			"sql": `
				SELECT 
					table_schema,
					table_name,
					table_type
				FROM information_schema.tables 
				WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
				AND table_type IN ('BASE TABLE', 'VIEW')
				ORDER BY table_schema, table_type, table_name;
			`,
		},
	}

	jsonBody, err = json.Marshal(allObjectsQuery)
	if err != nil {
		return fmt.Errorf("marshal objects request failed: %v", err)
	}

	dataEndpoint := strings.Replace(metadataEndpoint, "/v1/metadata", "/v2/query", 1)
	req, err = http.NewRequest("POST", dataEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return fmt.Errorf("create objects request failed: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	resp, err = client.Do(req)
	if err != nil {
		return fmt.Errorf("objects request failed: %v", err)
	}
	defer resp.Body.Close()

	var result struct {
		Result [][]string `json:"result"`
	}

	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Printf("Raw Response: %s\n", string(body))

	if err = json.Unmarshal(body, &result); err != nil {
		return fmt.Errorf("decode objects response failed: %v", err)
	}

	// 组织数据结构
	type SchemaObjects struct {
		Tables []string
		Views  []string
	}
	untracked := make(map[string]*SchemaObjects)

	// 跳过标题行
	if len(result.Result) > 1 {
		for _, row := range result.Result[1:] {
			schema := row[0]
			objName := row[1]
			objType := row[2]

			if _, exists := untracked[schema]; !exists {
				untracked[schema] = &SchemaObjects{
					Tables: make([]string, 0),
					Views:  make([]string, 0),
				}
			}

			if objType == "BASE TABLE" {
				untracked[schema].Tables = append(untracked[schema].Tables, objName)
			} else if objType == "VIEW" {
				untracked[schema].Views = append(untracked[schema].Views, objName)
			}
		}
	}

	// 输出结果
	fmt.Printf("\n数据源 '%s' 中未追踪的对象:\n", sourceName)
	fmt.Println("--------------------")

	for schema, objects := range untracked {
		if len(objects.Tables) > 0 || len(objects.Views) > 0 {
			fmt.Printf("\nSchema: %s\n", schema)
			if len(objects.Tables) > 0 {
				fmt.Println("  未追踪的表格:")
				for _, table := range objects.Tables {
					fmt.Printf("    - %s\n", table)
				}
			}
			if len(objects.Views) > 0 {
				fmt.Println("  未追踪的视图:")
				for _, view := range objects.Views {
					fmt.Printf("    - %s\n", view)
				}
			}
		}
	}

	fmt.Println("--------------------")
	return nil
}

// GetAllSources 获取所有数据源
func GetAllSources() ([]string, error) {
	// 获取所有数据源
	metadataQuery := map[string]interface{}{
		"type": "export_metadata",
		"args": map[string]interface{}{},
		"version": 2,
	}

	jsonBody, err := json.Marshal(metadataQuery)
	if err != nil {
		return nil, fmt.Errorf("元数据请求编码失败: %v", err)
	}

	req, err := http.NewRequest("POST", metadataEndpoint, bytes.NewBuffer(jsonBody))
	if err != nil {
		return nil, fmt.Errorf("创建元数据请求失败: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("元数据请求失败: %v", err)
	}
	defer resp.Body.Close()

	var result map[string]interface{}
	if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("解码元数据响应失败: %v", err)
	}

	var sources []string
	if metadata, ok := result["metadata"].(map[string]interface{}); ok {
		if sourcesList, ok := metadata["sources"].([]interface{}); ok {
			for _, source := range sourcesList {
				if sourceMap, ok := source.(map[string]interface{}); ok {
					if name, ok := sourceMap["name"].(string); ok {
						sources = append(sources, name)
					}
				}
			}
		}
	}

	return sources, nil
}

// TrackRelationship 追踪单个外键关系
func TrackRelationship(sourceName, schemaName, tableName, columnName, foreignSchemaName, foreignTableName, foreignColumnName, constraintName string) error {
    // 创建对象关系
    relationshipName := fmt.Sprintf("%s_%s_rel", tableName, columnName)
    createRelationshipQuery := map[string]interface{}{
        "type": "pg_create_object_relationship",
        "args": map[string]interface{}{
            "source": sourceName,
            "table": map[string]interface{}{
                "schema": schemaName,
                "name":   tableName,
            },
            "name": relationshipName,
            "using": map[string]interface{}{
                "foreign_key_constraint_on": columnName,
            },
        },
    }

    jsonBody, err := json.Marshal(createRelationshipQuery)
    if err != nil {
        return fmt.Errorf("关系请求编码失败: %v", err)
    }

    // 打印请求内容以便调试
    fmt.Printf("创建关系请求:\n%s\n", string(jsonBody))

    req, err := http.NewRequest("POST", metadataEndpoint, bytes.NewBuffer(jsonBody))
    if err != nil {
        return fmt.Errorf("创建关系请求失败: %v", err)
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("关系请求失败: %v", err)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("读取响应失败: %v", err)
    }

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("创建关系失败,状态码 %d: %s", resp.StatusCode, string(body))
    }

    fmt.Printf("成功创建关系: %s\n", relationshipName)
    return nil
}

// TrackUnTrackedRelationships 追踪所有未追踪的外键关系
func TrackUnTrackedRelationships(sourceName string) error {
    // 查询所有外键关系
    relationshipQuery := map[string]interface{}{
        "type": "run_sql",
        "args": map[string]interface{}{
            "source": sourceName,
            "sql": `
                SELECT
                    tc.table_schema as schema_name,
                    tc.constraint_name,
                    tc.table_name,
                    kcu.column_name,
                    ccu.table_schema AS foreign_schema_name,
                    ccu.table_name AS foreign_table_name,
                    ccu.column_name AS foreign_column_name
                FROM
                    information_schema.table_constraints AS tc
                    JOIN information_schema.key_column_usage AS kcu
                        ON tc.constraint_name = kcu.constraint_name
                        AND tc.table_schema = kcu.table_schema
                    JOIN information_schema.constraint_column_usage AS ccu
                        ON ccu.constraint_name = tc.constraint_name
                        AND ccu.table_schema = tc.table_schema
                WHERE
                    tc.constraint_type = 'FOREIGN KEY'
                    AND tc.table_schema NOT IN ('information_schema', 'pg_catalog')
                ORDER BY
                    tc.table_schema,
                    tc.table_name,
                    tc.constraint_name;
            `,
        },
    }

    dataEndpoint := strings.Replace(metadataEndpoint, "/v1/metadata", "/v2/query", 1)
    jsonBody, err := json.Marshal(relationshipQuery)
    if err != nil {
        return fmt.Errorf("关系请求编码失败: %v", err)
    }

    req, err := http.NewRequest("POST", dataEndpoint, bytes.NewBuffer(jsonBody))
    if err != nil {
        return fmt.Errorf("创建关系请求失败: %v", err)
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("关系请求失败: %v", err)
    }
    defer resp.Body.Close()

    var result struct {
        Result [][]string `json:"result"`
    }
    if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return fmt.Errorf("解码关系响应失败: %v", err)
    }

    // 获取已追踪的关系
    trackedQuery := map[string]interface{}{
        "type": "export_metadata",
        "args": map[string]interface{}{},
        "version": 2,
    }

    jsonBody, err = json.Marshal(trackedQuery)
    if err != nil {
        return fmt.Errorf("关系请求编码失败: %v", err)
    }

    req, err = http.NewRequest("POST", metadataEndpoint, bytes.NewBuffer(jsonBody))
    if err != nil {
        return fmt.Errorf("创建关系请求失败: %v", err)
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-Hasura-Admin-Secret", adminSecret)

    resp, err = client.Do(req)
    if err != nil {
        return fmt.Errorf("关系请求失败: %v", err)
    }
    defer resp.Body.Close()

    var trackedResult map[string]interface{}
    if err = json.NewDecoder(resp.Body).Decode(&trackedResult); err != nil {
        return fmt.Errorf("解码已追踪关系响应失败: %v", err)
    }

    fmt.Printf("\n追踪数据源 '%s' 中的外键关系:\n", sourceName)
    fmt.Println("--------------------")

    var trackedCount, untrackedCount, errorCount int

    if len(result.Result) > 1 { // 跳过标题行
        for _, row := range result.Result[1:] {
            schemaName := row[0]
            constraintName := row[1]
            tableName := row[2]
            columnName := row[3]
            foreignSchemaName := row[4]
            foreignTableName := row[5]
            foreignColumnName := row[6]

            // 检查关系是否已被追踪
            isTracked := false
            if sources, ok := trackedResult["metadata"].(map[string]interface{})["sources"].([]interface{}); ok {
                for _, source := range sources {
                    if sourceMap, ok := source.(map[string]interface{}); ok {
                        if sourceName == sourceMap["name"] {
                            if tables, ok := sourceMap["tables"].([]interface{}); ok {
                                for _, table := range tables {
                                    if tableMap, ok := table.(map[string]interface{}); ok {
                                        if tableInfo, ok := tableMap["table"].(map[string]interface{}); ok {
                                            if tableInfo["name"] == tableName && tableInfo["schema"] == schemaName {
                                                if relationships, ok := tableMap["object_relationships"].([]interface{}); ok {
                                                    for _, rel := range relationships {
                                                        if relMap, ok := rel.(map[string]interface{}); ok {
                                                            if using, ok := relMap["using"].(map[string]interface{}); ok {
                                                                if fKeyConstraint, ok := using["foreign_key_constraint_on"].(string); ok {
                                                                    if fKeyConstraint == columnName {
                                                                        isTracked = true
                                                                        trackedCount++
                                                                        break
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if !isTracked {
                fmt.Printf("追踪关系: %s.%s.%s -> %s.%s.%s (约束: %s)\n",
                    schemaName, tableName, columnName,
                    foreignSchemaName, foreignTableName, foreignColumnName,
                    constraintName)

                err := TrackRelationship(
                    sourceName,
                    schemaName,
                    tableName,
                    columnName,
                    foreignSchemaName,
                    foreignTableName,
                    foreignColumnName,
                    constraintName,
                )

                if err != nil {
                    fmt.Printf("追踪关系出错: %v\n", err)
                    errorCount++
                } else {
                    untrackedCount++
                }
            }
        }
    }

    fmt.Println("\n统计信息:")
    fmt.Printf("- 已追踪的关系: %d\n", trackedCount)
    fmt.Printf("- 新追踪的关系: %d\n", untrackedCount)
    fmt.Printf("- 追踪失败: %d\n", errorCount)
    fmt.Printf("- 关系总数: %d\n", trackedCount+untrackedCount+errorCount)
    fmt.Println("--------------------")

    return nil
}




func main() {
	fmt.Println("测试 Hasura 连接...")
	if err := TestHasuraConnection(); err != nil {
		fmt.Printf("连接测试失败: %v\n", err)
		os.Exit(1)
	}

	// 获取所有数据源
	sources, err := GetAllSources()
	if err != nil {
		fmt.Printf("获取数据源失败: %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("\n发现 %d 个数据源\n", len(sources))
	
	// 检查并追踪每个数据源中未追踪的对象
	for _, source := range sources {
		fmt.Printf("\n=== 处理数据源: %s ===\n", source)
		
		// 显示未追踪的对象
		if err := GetUnTrackedTablesAndViews(source); err != nil {
			fmt.Printf("获取未追踪对象失败,数据源 %s: %v\n", source, err)
			continue
		}

		// 追踪未追踪的对象
		if err := TrackUnTrackedObjects(source); err != nil {
			fmt.Printf("追踪对象失败,数据源 %s: %v\n", source, err)
			continue
		}
	}


    // 检查每个数据源中未追踪的关系
    // 处理每个数据源
    for _, source := range sources {
        fmt.Printf("\n=== 处理数据源: %s ===\n", source)
        
        // 显示未追踪的��系
		if err := GetUnTrackedTablesAndViews(source); err != nil {
            fmt.Printf("获取未追踪关系失败,数据源 %s: %v\n", source, err)
            continue
        }

        // 追踪未追踪的关系
        if err := TrackUnTrackedRelationships(source); err != nil {
            fmt.Printf("追踪关系失败,数据源 %s: %v\n", source, err)
            continue
        }
    }


	fmt.Println("\n处理完成!")
}


一文读懂资管系统里的“清算路径”:钱和券的“导航地图” 简单来说,清算路径就是给每一笔交易预设好的“导航路线”。钱从哪个银行账户出?通过哪家证券公司去买?用哪个股东账户去登记?买回来的股票记在谁的名下?“清算路径”就是把这一连串问题的答案提前配置好。它连接了银行托管户(钱的源头)、券商(交易的中介)和交易所(买卖的市场)。没有配好这条路,交易就无法闭环。清算路径是金融基础设施的缩影。资金如何安全地从银行流向市场;资产如何合法地归属到产品名下;交易数据如何在不同系统间流转。它是资管系统能够日复一日、准确无误地处理亿万级交易的基石。 阅读详情

相关推荐

MaaS 商业模式全面分析报告:从技术赋能到商业变现的全链条探索

MaaS(模型即服务)商业模式正成为AI产业主流,2024年上半年中国市场规模达2.5亿元,预计2027年占比将提升至47%。该模式通过云服务提供预训练AI模型,大幅降低企业应用AI的技术门槛和成本。核心盈利方式包括按使用量计费、订阅制、定制化服务等,其中混合模式因灵活性成为趋势。MaaS产业链包含模型提供商、云平台、应用开发商和最终用户,重塑了AI产业格局。随着《数据法案》等监管加强,数据增值服务面临更严格合规要求。未来五年,中国MaaS市场增速预计超50%,用户规模将从2025年2.1亿增至2030年5

芯片行业20年 · 参与定义实现千万级量产出货 · AI芯片PM 2892

使用 Hasura 和 PostgreSQL 构建后端技巧你会吗

后端开发通常需要编写多行代码来处理 CRUD、授权和业务逻辑。所有这些代码都需要在项目的整个生命周期内进行测试、调试和维护。这需要开发人员可以用来开发新功能的大量时间。在本文中,您将了解 Hasura 和 PostgreSQL 如何帮助您加快应用程序开发并以最少的工作快速启动后端。 Hasura是一个开源 GraphQL 引擎,它使用您的数据库模式生成的 GraphQL 和 REST API 端点。它支持数据建模、实时查询、事件编程、基于角色的授权以及通过 GraphQL 运行自定义业务逻辑的操作。 P

qq_39652397的博客 3791

避开SM30维护视图的坑:自动更新字段+日志记录的5个关键配置步骤

本文深入解析了SAP ABAP开发中SM30维护视图的深度定制,聚焦于实现自动更新字段与日志记录功能。文章详细阐述了从SE54配置起点到核心编码的5个关键步骤,重点讲解了如何利用事件驱动模型(如05创建事件和01保存前事件)安全操作动态数据结构,并提供了避免常见陷阱的实用技巧,帮助开发者构建稳定可靠的自动化维护机制。

plant的博客 623

GraphQL入门与开源的GraphQL引擎Hasura体验

Hasura 是一个开源的 GraphQL 引擎,它可以帮助开发人员快速构建和部署现代应用程序的后端。它提供了一个自动化的 GraphQL API ,可以直接连接到现有的数据库,并提供实时数据推送和订阅功能。 Hasura 团队总部位于印度。作为一款后端低代码平台,也推荐使用对标 Hasura 的国产替代产品:飞布:飞布是可视化API开发平台,对标 hasura ,灵活开放、多语言兼容、简单易学,能构建生产级 WEB API ,让前端变全栈,让后端不搬砖。

Heartsuit的博客 3083

Spring Boot应用Docker化实战:从Dockerfile编写到镜像推送完整流程

本文详细介绍了将Spring Boot应用Docker化的完整实战流程。从环境准备、项目审视,到编写高效且安全的Dockerfile,重点讲解了多阶段构建、镜像层优化、Spring Boot分层JAR等核心技巧,并涵盖了镜像构建、验证、调试以及最终推送至仓库并集成CI/CD的最佳实践,帮助开发者构建生产级Docker镜像。

butter的博客 1023

捷联惯导总结--初始对准,位置标定,INS姿态更新,GPS/INS组合

惯导及组合导航回顾  2018.09.16 今天和17系的同学一起把惯导的流程捋了一遍,为了加深自己的记忆,这里在前面把心得大致列出来。 我们这里只考虑捷联式惯导及松组合 首先拿到惯性传感器(加速度计和陀螺仪)需要对其进行标定,是为了求出零偏,比例因子,交轴耦合这些误差。这些误差有的虽然在出厂时会给出,但是考虑到实际环境不同,还是要做标定。一般采用的是六位置旋转法,针对加速度计和陀螺仪同时...

赤豆几维 5万+

探索graphQL-hasura引擎

graphql作为一种API规范,hasura是对graphql规范的实现,它提供了快速暴露api和角色数据权限的能力。适用于绝大多数接口直接读写数据表的场景。

陈海龙的格物之路 1593

多物理场光学仿真软件:Ansys Lumerical_(8).CHARGE解决方案详解

CHARGE 模块主要用于求解 Poisson 方程和连续性方程,以模拟半导体器件中的电荷分布和载流子输运。通过 CHARGE 模块,可以分析器件的能带结构、载流子浓度、电流密度等关键参数。这些参数对于理解器件的工作原理和优化设计至关重要。

kkchenjj的博客 1390

简单的自托管hasura

With HTTPS, Docker, Digital Ocean, Google Domains使用HTTPS,Docker,Digital Ocean,Google Domains Hasura is an awesome tool to rapidly prototype a back end api. Hasura cloud is a newer product of theirs th...

weixin_26716079的博客 1140

【PyQt】使用PyQt6基于Scenedetect制作视频场景自动分割应用

通过详细介绍PySceneDetect的功能和应用,本文阐释了如何利用这一工具提高视频编辑的效率。我们讨论了安装方法、命令行使用示例以及如何在Python脚本中集成PySceneDetect。此外文章还涵盖了基于PyQt6的图形界面设计,通过多线程技术处理视频,保证界面的响应性,并通过实时反馈机制提升用户体验。无论是专业的视频制作团队还是个人爱好者,PySceneDetect都提供了一种可行的、效率高的场景检测工具,让视频编辑工作变得更加简单快捷。

Mr数据杨 962

QT-Creator安装教程(windows)

本文介绍了Qt的下载安装步骤:1)通过镜像源下载安装包;2)运行exe文件时使用命令行参数指定镜像地址;3)在安装过程中选择路径和自定义安装组件,包含QT5和QT6版本的详细安装说明;4)安装后可继续添加扩展包或卸载组件。重点提示了QT6必须勾选的关键组件,并提供了组件管理的操作方法。

qq_53545309的博客 3107

hasura_使用Hasura构建GraphQL应用

hasuraToday in this article we are going to build a secure todo list using GraphQL API with Hasura. First you have to know about Hasura. It is nothing but a postgreSQL database that automatically give...

weixin_26750481的博客 4258

SAP Joule 在顾问与开发者场景的双擎定位:Joule for Consultants 与 Joule for Developers 全面解读

SAP推出企业级AI助手Joule,分为顾问版(Joule for Consultants)和开发者版(Joule for Developers)两个版本。顾问版聚焦咨询业务,整合SAP知识库和方法论,辅助方案设计、差距分析和实施落地;开发者版嵌入开发工具链,支持代码生成、解释、测试和迁移。两者共享智能底座但面向不同场景:顾问版优化项目交付流程,开发者版提升工程效率。典型案例显示,Joule能有效缩短云转型周期,实现从业务咨询到技术落地的无缝衔接。该产品通过AI代理和知识图谱技术,为企业数字化转型提供端到端

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 354

Hasura Event Triggers 实战:用 Go 编写 AWS Lambda Webhook,在数据库事件上执行 GraphQL Mutation

本指南基于 [graphql-engine 仓库](https://link.gitcode.com/i/b00624471ee9985983755e3f1ab92619) 中的 Go 语言 AWS Lambda 事件触发器模板([mutation 示例](https://link.gitcode.com/i/4e759ecb600c411a88205008b1d3697a)),讲解如何编写一个部

gitblog_00320的博客 620

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Hasura 迁移文件清洗指南:解析 hasura-edit-pg-dump 工具与 pg_dump 输出净化原理

本文围绕 Hasura GraphQL Engine 仓库中 `scripts/edit-pg-dump` 工具展开,深入讲解如何将 `pg_dump` 导出的原始 SQL 清洗为可直接用于 Hasura 迁移(migration)的干净文件。读完本文,你将掌握该工具的四种清洗规则、Go 服务端与 Bash 两种实现细节、部署与调用方式,以及 Hasura 服务端 `clean_output` 参

gitblog_00840的博客 391

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

@hasura/metadata-api:基于 OpenAPI 规范构建的 Hasura Metadata API 类型体系

Hasura GraphQL Engine 的一切配置(数据源、权限、事件触发器、远程 Schema 等)都通过 Metadata API 来描述,而 `@hasura/metadata-api` 正是这一配置面的 TypeScript 类型库:它把引擎对外暴露的 OpenAPI 规范自动生成为类型化的 TS 定义,让开发者可以用强类型的方式导出、导入和操作整个 metadata。本文将围绕该包的

gitblog_00391的博客 413

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

Hasura GraphQL Engine 深度指南:在 Postgres 之上构建即时、实时的 GraphQL API

本文以官方葡萄牙语 README([translations/README.portuguese_br.md](https://link.gitcode.com/i/ceb594bba6d47ac37f8b9a343b8193f9))为主体脉络,系统讲解 Hasura GraphQL Engine 的核心能力、一键部署方式、整体架构,以及通过远程 schema、事件触发器扩展业务逻辑的实战方法,并

gitblog_00711的博客 427

Hasura GraphQL Engine 入门指南:在 Postgres 之上构建实时 GraphQL API 并扩展业务逻辑

本指南以仓库中的日文版 README([translations/README.japanese.md](https://link.gitcode.com/i/7c801ac6401a6de7a10dbbaa7b7deecf))为骨架,系统讲解 Hasura GraphQL Engine(V2 引擎)的核心能力、一键部署与 Docker 部署方式、引擎架构,以及通过远程模式(Remote Sche

gitblog_00220的博客 217

dots-tts-mlx-mf-int8完全指南:Apple Silicon本地化48kHz高保真文本转语音新选择

dots-tts-mlx-mf-int8 是一个面向 **Apple Silicon(M 系列芯片 Mac)** 的本地化文本转语音(TTS)模型仓库,基于 MLX 框架深度优化,输出 48kHz 高保真音频。它集成了 MeanFlow 快速推理与 INT8 量化技术,让大语言模型级别的 TTS 首次能够以"低内存、少步数、高音质"的形式跑在 Mac 本地,隐私数据不出设备。 无论你是想给视频配

gitblog_00578的博客 219

azure api 管理_在Azure上使用Hasura设置postgres和graphql api

azure api 管理Key Technologies: Hasura, Postgres, Terraform, Docker, and Azure. 关键技术: Hasura , Postgres , Terraform , Docker和Azure 。 I created a data model to store railroad systems, services, schedule...

weixin_26712121的博客 507

Hasura GraphQL Engine 常见问题解决方案

Hasura GraphQL Engine 常见问题解决方案 【免费下载链接】graphql-engine GraphQL Engine: 是一个开源的 GraphQL 服务器,用于实现 GraphQL API。它可以帮助开发者快速构建 API,支持数据查询、数据变更和实时数据更新等功能。特点包括易定制、高性能、支持多种...

gitblog_01034的博客 546

Hasura GraphQL Engine:为数据库一键生成 GraphQL API

Hasura GraphQL Engine 是一个开源项目,能够自动为数据库生成 GraphQL API,无需编写后端代码。它支持 PostgreSQL、MongoDB 等多种数据库,提供 CRUD 查询、实时订阅、权限控制等功能。目前维护 V2(稳定版)和 V3(新架构)两个版本,V3 支持通过 Connector SDK 扩展数据源。项目采用 Apache 2.0 和 MIT 许可证,提供 Docker 快速部署方案,适用于快速构建数据 API 的场景。

mofashu621319的博客 255

BigBlueButton bbb-graphql-server 开发环境搭建指南:基于 Hasura 的实时 GraphQL 服务从安装到调试

BigBlueButton 在 `bbb-graphql-server` 模块中引入了一套基于 **Hasura GraphQL Engine + PostgreSQL** 的实时 GraphQL 数据层,让会议中的用户、聊天、投票、白板、分组讨论等状态以可订阅的 GraphQL 查询形式暴露给 HTML5 客户端。本文以仓库内 [bbb-graphql-server/README.md](htt

gitblog_00460的博客 465

Hasura GraphQL Engine:一个端点搞定所有数据查询

摘要: Hasura GraphQL Engine(3.2万Star)通过GraphQL端点自动将数据库表、视图等映射为API,省去手写REST接口的繁琐工作,支持PostgreSQL、MongoDB等主流数据库。其核心优势包括: 高效开发:自动生成CRUD接口,提升10倍接入速度; 开源生态:Apache 2.0协议,支持自托管与扩展,社区活跃; 灵活适配:适合快速迭代的中小团队,复杂逻辑可通过Connector SDK扩展。 注意:深度定制需求需结合自定义函数,且项目为Monorepo结构,建议浅克隆。

wangtestbbb44的博客 223

Hasura GraphQL Engine 远程模式(Remote Schemas)完全指南:合并自定义 GraphQL 服务到统一 API

Hasura GraphQL Engine 在提供带授权与访问控制的 CRUD + 实时 GraphQL API 之外,还内置了"远程模式"(Remote Schemas)能力:把任意语言或框架编写的自定义 GraphQL 服务,通过自动化的模式合并(automated schema merging)统一到同一个 GraphQL 端点上。本文基于仓库中的 [remote-schemas.chine

gitblog_00061的博客 341

在 Zeit Now 上用 Node.js 部署 Hasura 事件触发器:从 echo 回显到 GraphQL 回写数据库的完整实战指南

本篇技术指南围绕 [community/boilerplates/event-triggers/zeit-now/nodejs](https://link.gitcode.com/i/9f94bb4be1be5fc448609a5d3efddbb2) 中的两个 Node.js 样板展开:**echo**(回显事件负载)与 **mutation**(在事件回调中通过 GraphQL 写回数据库)。读

gitblog_00953的博客 434
上一篇: go 使用fyne实现桌面程序的计算器例子
下一篇: 基于go中fyne gui的通达信数据导出工具
布林模型
博客等级 码龄15年 61粉丝 25原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值