Skip to content

Commit ccde34a

Browse files
committed
schema import
1 parent 2d47e42 commit ccde34a

27 files changed

Lines changed: 1444 additions & 873 deletions

cmd/gen.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var generateCmd = &cobra.Command{
1919
}
2020

2121
func generateAPI() {
22-
if err := schema.Generate(configPath, false, saveFiles); err != nil {
22+
if err := schema.Generate(configPath, false, saveFiles, "server.go"); err != nil {
2323
fmt.Fprintln(os.Stderr, "failed to load config", err.Error())
2424
os.Exit(2)
2525
}

cmd/import.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/roneli/fastgql/pkg/importer"
10+
"github.com/roneli/fastgql/pkg/importer/postgres"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var (
15+
importConnStr string
16+
importSchemaName string
17+
importTables []string
18+
importOutputFile string
19+
importGenerateQueries bool
20+
importGenerateFilters bool
21+
)
22+
23+
var importCmd = &cobra.Command{
24+
Use: "import",
25+
Short: "import GraphQL schema from a PostgreSQL database",
26+
Long: `Import and generate a GraphQL schema from an existing PostgreSQL database by introspecting the database structure`,
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
// Check for DATABASE_URL environment variable if conn string not provided
29+
if importConnStr == "" {
30+
if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" {
31+
importConnStr = dbURL
32+
} else {
33+
return fmt.Errorf("connection string is required (use --conn or set DATABASE_URL)")
34+
}
35+
}
36+
37+
ctx := context.Background()
38+
39+
// Create PostgreSQL source
40+
source := postgres.NewSource(nil)
41+
if err := source.Connect(ctx, importConnStr); err != nil {
42+
return fmt.Errorf("failed to connect to database: %w", err)
43+
}
44+
defer source.Close()
45+
46+
// Build options
47+
options := importer.IntrospectOptions{
48+
SchemaName: importSchemaName,
49+
Tables: importTables,
50+
GenerateQueries: importGenerateQueries,
51+
GenerateFilters: importGenerateFilters,
52+
}
53+
54+
// Introspect database
55+
schema, err := source.Introspect(ctx, options)
56+
if err != nil {
57+
return fmt.Errorf("failed to introspect database: %w", err)
58+
}
59+
60+
// Generate GraphQL schema
61+
astSource, err := importer.GenerateSchema(schema)
62+
if err != nil {
63+
return fmt.Errorf("failed to generate schema: %w", err)
64+
}
65+
66+
// Determine output file
67+
outputFile := importOutputFile
68+
if outputFile == "" {
69+
outputFile = "graph/schema.graphql"
70+
}
71+
72+
// Create directory if needed
73+
if err := os.MkdirAll(filepath.Dir(outputFile), 0755); err != nil {
74+
return fmt.Errorf("failed to create output directory: %w", err)
75+
}
76+
77+
// Write schema to file
78+
if err := os.WriteFile(outputFile, []byte(astSource.Input), 0644); err != nil {
79+
return fmt.Errorf("failed to write schema file: %w", err)
80+
}
81+
82+
fmt.Printf("Successfully imported schema from PostgreSQL database\n")
83+
fmt.Printf("Schema written to: %s\n", outputFile)
84+
fmt.Printf("Found %d object types\n", len(schema.ObjectTypes))
85+
if len(schema.QueryFields) > 0 {
86+
fmt.Printf("Generated %d query fields\n", len(schema.QueryFields))
87+
}
88+
89+
return nil
90+
},
91+
}
92+
93+
func init() {
94+
importCmd.Flags().StringVarP(&importConnStr, "conn", "d", "", "PostgreSQL connection string (or set DATABASE_URL environment variable)")
95+
importCmd.Flags().StringVarP(&importSchemaName, "schema", "s", "public", "Database schema name to introspect")
96+
importCmd.Flags().StringSliceVarP(&importTables, "tables", "t", []string{}, "Comma-separated list of table names to import (empty = all tables)")
97+
importCmd.Flags().StringVarP(&importOutputFile, "output", "o", "", "Output file path for generated schema (default: graph/schema.graphql)")
98+
importCmd.Flags().BoolVarP(&importGenerateQueries, "queries", "q", false, "Generate query fields for all tables")
99+
importCmd.Flags().BoolVarP(&importGenerateFilters, "filters", "f", false, "Generate filter inputs for all types")
100+
}

cmd/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ var initCmd = &cobra.Command{
118118
return err
119119
}
120120
}
121-
if err := schema.Generate(configFilename, true, false); err != nil {
121+
if err := schema.Generate(configFilename, true, false, serverFilename); err != nil {
122122
fmt.Fprintln(os.Stderr, "failed to load config", err.Error())
123123
os.Exit(2)
124124
}

cmd/root.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
)
99

1010
var (
11-
verbose bool
1211
configPath string
1312
configFilename string
1413
schemaFilename string
@@ -22,9 +21,8 @@ var (
2221
)
2322

2423
func init() {
25-
rootCmd.AddCommand(generateCmd, versionCmd, initCmd)
24+
rootCmd.AddCommand(generateCmd, versionCmd, initCmd, importCmd)
2625
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to server config")
27-
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "turns fastgql build log on")
2826
initCmd.Flags().StringVarP(&schemaFilename, "schemaName", "s", "schema.graphql", "name of schema file")
2927
initCmd.Flags().StringVarP(&configFilename, "configName", "n", "gqlgen.yml", "name of config file")
3028
initCmd.Flags().StringVarP(&serverFilename, "serverName", "g", "server.go", "name of server file")

pkg/execution/builders/field.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ func parseFieldType(field *ast.Field, typeDef *ast.Definition) fieldType {
321321
case strings.HasSuffix(field.Name, "Aggregate"):
322322
return TypeAggregate
323323
case typeDef.IsCompositeType():
324-
if d := field.Definition.Directives.ForName("relation"); d != nil {
324+
if d := field.Definition.Directives.ForName(schema.RelationDirectiveName); d != nil {
325325
return TypeRelation
326326
}
327327
return TypeObject

pkg/execution/builders/sql/builder.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,9 @@ func (b Builder) buildQuery(tableDef tableDefinition, field builders.Field) (*qu
268268
// if type is abstract check if it has a typename
269269
if field.TypeDefinition.IsAbstractType() {
270270
// add type name field
271-
d := field.TypeDefinition.Directives.ForName("typename")
271+
d := field.TypeDefinition.Directives.ForName(schema.TypenameDirectiveName)
272272
if d != nil {
273-
name := d.Arguments.ForName("name")
273+
name := d.Arguments.ForName(schema.ArgNameTable)
274274
query.selects = append(query.selects, column{table: query.alias, name: b.CaseConverter(name.Value.Raw), alias: name.Value.Raw})
275275
b.Logger.Debug("adding typename field for interface", "interface", field.TypeDefinition.Name, "tableDefinition", tableDef.name, "fieldName", name.Value.Raw)
276276
fieldsAdded[name.Value.Raw] = struct{}{}
@@ -498,7 +498,7 @@ func (b Builder) buildFilterExp(table tableHelper, astDefinition *ast.Definition
498498
return nil, fmt.Errorf("fatal value of bool exp not map")
499499
}
500500
fid := filterInputDef.Fields.ForName(k)
501-
if fid.Directives.ForName("isInterfaceFilter") != nil {
501+
if fid.Directives.ForName(schema.IsInterfaceFilterDirectiveName) != nil {
502502
// add type filter + interface filter
503503
expBuilder = expBuilder.Append(b.buildInterfaceFilter(table, astDefinition, b.Schema.Types[strcase.ToCamel(k)], kv))
504504
continue
@@ -653,7 +653,7 @@ func (b Builder) buildFilterQuery(parentTable tableHelper, rf *ast.Definition, r
653653
}
654654

655655
func (b Builder) buildInterfaceFilter(table tableHelper, parentDef, definition *ast.Definition, kv map[string]any) goqu.Expression {
656-
d := parentDef.Directives.ForName("typename").Arguments.ForName("name").Value.Raw
656+
d := parentDef.Directives.ForName(schema.TypenameDirectiveName).Arguments.ForName(schema.ArgNameTable).Value.Raw
657657
filterExp, err := b.buildFilterExp(table, definition, kv)
658658
if err != nil {
659659
panic(err)

pkg/execution/builders/sql/directives.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/doug-martin/goqu/v9"
88
"github.com/doug-martin/goqu/v9/exp"
99
"github.com/jinzhu/inflection"
10+
schemapkg "github.com/roneli/fastgql/pkg/schema"
1011
"github.com/vektah/gqlparser/v2/ast"
1112
)
1213

@@ -61,16 +62,16 @@ func getTableName(schema *ast.Schema, typeName, fieldName string) tableDefinitio
6162
objType: nil,
6263
}
6364
}
64-
d := objType.Directives.ForName("table")
65+
d := objType.Directives.ForName(schemapkg.TableDirectiveName)
6566
if d == nil {
6667
return tableDefinition{
6768
name: strings.ToLower(objType.Name),
6869
schema: "",
6970
objType: objType,
7071
}
7172
}
72-
name := d.Arguments.ForName("name").Value.Raw
73-
schemaValue := d.Arguments.ForName("schema")
73+
name := d.Arguments.ForName(schemapkg.ArgNameTable).Value.Raw
74+
schemaValue := d.Arguments.ForName(schemapkg.ArgNameSchema)
7475
if schemaValue == nil {
7576
return tableDefinition{
7677
name: name,

0 commit comments

Comments
 (0)