@sqldoc/templates 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +161 -0
- package/src/__tests__/dedent.test.ts +45 -0
- package/src/__tests__/docker-templates.test.ts +134 -0
- package/src/__tests__/go-structs.test.ts +184 -0
- package/src/__tests__/naming.test.ts +48 -0
- package/src/__tests__/python-dataclasses.test.ts +185 -0
- package/src/__tests__/rust-structs.test.ts +176 -0
- package/src/__tests__/tags-helpers.test.ts +72 -0
- package/src/__tests__/type-mapping.test.ts +332 -0
- package/src/__tests__/typescript.test.ts +202 -0
- package/src/cobol-copybook/index.ts +220 -0
- package/src/cobol-copybook/test/.gitignore +6 -0
- package/src/cobol-copybook/test/Dockerfile +7 -0
- package/src/csharp-records/index.ts +131 -0
- package/src/csharp-records/test/.gitignore +6 -0
- package/src/csharp-records/test/Dockerfile +6 -0
- package/src/diesel/index.ts +247 -0
- package/src/diesel/test/.gitignore +6 -0
- package/src/diesel/test/Dockerfile +16 -0
- package/src/drizzle/index.ts +255 -0
- package/src/drizzle/test/.gitignore +6 -0
- package/src/drizzle/test/Dockerfile +8 -0
- package/src/drizzle/test/test.ts +71 -0
- package/src/efcore/index.ts +190 -0
- package/src/efcore/test/.gitignore +6 -0
- package/src/efcore/test/Dockerfile +7 -0
- package/src/go-structs/index.ts +119 -0
- package/src/go-structs/test/.gitignore +6 -0
- package/src/go-structs/test/Dockerfile +13 -0
- package/src/go-structs/test/test.go +71 -0
- package/src/gorm/index.ts +134 -0
- package/src/gorm/test/.gitignore +6 -0
- package/src/gorm/test/Dockerfile +13 -0
- package/src/gorm/test/test.go +65 -0
- package/src/helpers/atlas.ts +43 -0
- package/src/helpers/enrich.ts +396 -0
- package/src/helpers/naming.ts +19 -0
- package/src/helpers/tags.ts +63 -0
- package/src/index.ts +24 -0
- package/src/java-records/index.ts +179 -0
- package/src/java-records/test/.gitignore +6 -0
- package/src/java-records/test/Dockerfile +11 -0
- package/src/java-records/test/Test.java +93 -0
- package/src/jpa/index.ts +279 -0
- package/src/jpa/test/.gitignore +6 -0
- package/src/jpa/test/Dockerfile +14 -0
- package/src/jpa/test/Test.java +111 -0
- package/src/json-schema/index.ts +351 -0
- package/src/json-schema/test/.gitignore +6 -0
- package/src/json-schema/test/Dockerfile +18 -0
- package/src/knex/index.ts +168 -0
- package/src/knex/test/.gitignore +6 -0
- package/src/knex/test/Dockerfile +7 -0
- package/src/knex/test/test.ts +75 -0
- package/src/kotlin-data/index.ts +147 -0
- package/src/kotlin-data/test/.gitignore +6 -0
- package/src/kotlin-data/test/Dockerfile +14 -0
- package/src/kotlin-data/test/Test.kt +82 -0
- package/src/kysely/index.ts +165 -0
- package/src/kysely/test/.gitignore +6 -0
- package/src/kysely/test/Dockerfile +8 -0
- package/src/kysely/test/test.ts +82 -0
- package/src/prisma/index.ts +387 -0
- package/src/prisma/test/.gitignore +6 -0
- package/src/prisma/test/Dockerfile +7 -0
- package/src/protobuf/index.ts +219 -0
- package/src/protobuf/test/.gitignore +6 -0
- package/src/protobuf/test/Dockerfile +6 -0
- package/src/pydantic/index.ts +272 -0
- package/src/pydantic/test/.gitignore +6 -0
- package/src/pydantic/test/Dockerfile +8 -0
- package/src/pydantic/test/test.py +63 -0
- package/src/python-dataclasses/index.ts +217 -0
- package/src/python-dataclasses/test/.gitignore +6 -0
- package/src/python-dataclasses/test/Dockerfile +8 -0
- package/src/python-dataclasses/test/test.py +63 -0
- package/src/rust-structs/index.ts +152 -0
- package/src/rust-structs/test/.gitignore +6 -0
- package/src/rust-structs/test/Dockerfile +22 -0
- package/src/rust-structs/test/test.rs +82 -0
- package/src/sqlalchemy/index.ts +258 -0
- package/src/sqlalchemy/test/.gitignore +6 -0
- package/src/sqlalchemy/test/Dockerfile +8 -0
- package/src/sqlalchemy/test/test.py +61 -0
- package/src/sqlc/index.ts +148 -0
- package/src/sqlc/test/.gitignore +6 -0
- package/src/sqlc/test/Dockerfile +13 -0
- package/src/sqlc/test/test.go +91 -0
- package/src/tags/dedent.ts +28 -0
- package/src/tags/index.ts +14 -0
- package/src/types/index.ts +8 -0
- package/src/types/pg-to-csharp.ts +136 -0
- package/src/types/pg-to-go.ts +120 -0
- package/src/types/pg-to-java.ts +141 -0
- package/src/types/pg-to-kotlin.ts +119 -0
- package/src/types/pg-to-python.ts +120 -0
- package/src/types/pg-to-rust.ts +121 -0
- package/src/types/pg-to-ts.ts +173 -0
- package/src/typescript/index.ts +168 -0
- package/src/typescript/test/.gitignore +6 -0
- package/src/typescript/test/Dockerfile +8 -0
- package/src/typescript/test/test.ts +89 -0
- package/src/xsd/index.ts +191 -0
- package/src/xsd/test/.gitignore +6 -0
- package/src/xsd/test/Dockerfile +6 -0
- package/src/zod/index.ts +289 -0
- package/src/zod/test/.gitignore +6 -0
- package/src/zod/test/Dockerfile +6 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
|
|
8
|
+
"github.com/jackc/pgx/v5"
|
|
9
|
+
|
|
10
|
+
"sqldoc-test/models"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
var failed int
|
|
14
|
+
|
|
15
|
+
func assert(condition bool, msg string) {
|
|
16
|
+
if !condition {
|
|
17
|
+
fmt.Fprintf(os.Stderr, "FAIL: %s\n", msg)
|
|
18
|
+
failed++
|
|
19
|
+
} else {
|
|
20
|
+
fmt.Printf(" ok: %s\n", msg)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func main() {
|
|
25
|
+
ctx := context.Background()
|
|
26
|
+
dbURL := os.Getenv("DATABASE_URL")
|
|
27
|
+
if dbURL == "" {
|
|
28
|
+
fmt.Fprintln(os.Stderr, "DATABASE_URL not set")
|
|
29
|
+
os.Exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
conn, err := pgx.Connect(ctx, dbURL)
|
|
33
|
+
if err != nil {
|
|
34
|
+
fmt.Fprintf(os.Stderr, "connect error: %v\n", err)
|
|
35
|
+
os.Exit(1)
|
|
36
|
+
}
|
|
37
|
+
defer conn.Close(ctx)
|
|
38
|
+
|
|
39
|
+
fmt.Println("--- go-structs integration test ---")
|
|
40
|
+
|
|
41
|
+
// 1. Query user into generated struct
|
|
42
|
+
var user models.Users
|
|
43
|
+
err = conn.QueryRow(ctx, "SELECT id, email, name, age, is_active FROM users WHERE id = 1").
|
|
44
|
+
Scan(&user.Id, &user.Email, &user.Name, &user.Age, &user.IsActive)
|
|
45
|
+
if err != nil {
|
|
46
|
+
fmt.Fprintf(os.Stderr, "query user error: %v\n", err)
|
|
47
|
+
os.Exit(1)
|
|
48
|
+
}
|
|
49
|
+
assert(user.Email == "test@example.com", "user.Email matches")
|
|
50
|
+
assert(user.Name != nil && *user.Name == "Test User", "user.Name matches")
|
|
51
|
+
assert(user.Age != nil && *user.Age == 30, "user.Age matches")
|
|
52
|
+
assert(user.IsActive == true, "user.IsActive matches")
|
|
53
|
+
|
|
54
|
+
// 2. Query post into generated struct
|
|
55
|
+
var post models.Posts
|
|
56
|
+
err = conn.QueryRow(ctx, "SELECT id, user_id, title, body, view_count FROM posts WHERE id = 1").
|
|
57
|
+
Scan(&post.Id, &post.UserId, &post.Title, &post.Body, &post.ViewCount)
|
|
58
|
+
if err != nil {
|
|
59
|
+
fmt.Fprintf(os.Stderr, "query post error: %v\n", err)
|
|
60
|
+
os.Exit(1)
|
|
61
|
+
}
|
|
62
|
+
assert(post.Title == "Hello World", "post.Title matches")
|
|
63
|
+
assert(post.UserId == 1, "post.UserId matches")
|
|
64
|
+
assert(post.ViewCount == 42, "post.ViewCount matches")
|
|
65
|
+
|
|
66
|
+
if failed > 0 {
|
|
67
|
+
fmt.Fprintf(os.Stderr, "\n%d assertion(s) failed\n", failed)
|
|
68
|
+
os.Exit(1)
|
|
69
|
+
}
|
|
70
|
+
fmt.Println("\nAll assertions passed!")
|
|
71
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { defineTemplate } from '@sqldoc/ns-codegen'
|
|
2
|
+
import { activeTables, enrichRealm } from '../helpers/enrich.ts'
|
|
3
|
+
import { toPascalCase } from '../helpers/naming.ts'
|
|
4
|
+
import { pgToGo } from '../types/pg-to-go.ts'
|
|
5
|
+
|
|
6
|
+
export default defineTemplate({
|
|
7
|
+
name: 'GORM Models',
|
|
8
|
+
description: 'Generate Go structs with GORM struct tags from SQL schema',
|
|
9
|
+
language: 'go',
|
|
10
|
+
|
|
11
|
+
generate(ctx) {
|
|
12
|
+
const schema = enrichRealm(ctx)
|
|
13
|
+
const allImports = new Set<string>()
|
|
14
|
+
const structBlocks: string[] = []
|
|
15
|
+
|
|
16
|
+
// Enums
|
|
17
|
+
for (const e of schema.enums) {
|
|
18
|
+
const typeName = toPascalCase(e.name)
|
|
19
|
+
structBlocks.push(`type ${typeName} string`)
|
|
20
|
+
structBlocks.push('')
|
|
21
|
+
const constLines = e.values.map((v) => {
|
|
22
|
+
const constName = `${typeName}${toPascalCase(v)}`
|
|
23
|
+
return `\t${constName} ${typeName} = "${v}"`
|
|
24
|
+
})
|
|
25
|
+
structBlocks.push(`const (\n${constLines.join('\n')}\n)`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Composite types (collected from columns)
|
|
29
|
+
const composites = new Map<string, Array<{ name: string; type: string }>>()
|
|
30
|
+
for (const table of schema.tables) {
|
|
31
|
+
for (const col of table.columns) {
|
|
32
|
+
if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
|
|
33
|
+
composites.set(col.pgType, col.compositeFields)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const view of schema.views) {
|
|
38
|
+
for (const col of view.columns) {
|
|
39
|
+
if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
|
|
40
|
+
composites.set(col.pgType, col.compositeFields)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const [name, fields] of composites) {
|
|
45
|
+
const typeName = toPascalCase(name)
|
|
46
|
+
const fieldLines = fields.map((f) => {
|
|
47
|
+
const mapped = pgToGo(f.type, false)
|
|
48
|
+
for (const imp of mapped.imports) allImports.add(imp)
|
|
49
|
+
const jsonTag = `json:"${f.name}"`
|
|
50
|
+
return `\t${toPascalCase(f.name)} ${mapped.type} \`${jsonTag}\``
|
|
51
|
+
})
|
|
52
|
+
structBlocks.push(`type ${typeName} struct {\n${fieldLines.join('\n')}\n}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Helper to resolve Go type for a column
|
|
56
|
+
function resolveGoType(col: any): string {
|
|
57
|
+
if (col.typeOverride) return col.typeOverride
|
|
58
|
+
if (col.category === 'enum' && col.enumValues?.length) {
|
|
59
|
+
return col.nullable ? `*${toPascalCase(col.pgType)}` : toPascalCase(col.pgType)
|
|
60
|
+
}
|
|
61
|
+
if (col.category === 'composite' && col.compositeFields?.length) {
|
|
62
|
+
return col.nullable ? `*${toPascalCase(col.pgType)}` : toPascalCase(col.pgType)
|
|
63
|
+
}
|
|
64
|
+
const mapped = pgToGo(col.pgType, col.nullable, col.category)
|
|
65
|
+
for (const imp of mapped.imports) allImports.add(imp)
|
|
66
|
+
return mapped.type
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const table of activeTables(schema)) {
|
|
70
|
+
const fields: string[] = []
|
|
71
|
+
|
|
72
|
+
for (const col of table.columns) {
|
|
73
|
+
const goType = resolveGoType(col)
|
|
74
|
+
|
|
75
|
+
// Build GORM tag parts
|
|
76
|
+
const gormParts: string[] = []
|
|
77
|
+
if (col.isPrimaryKey) gormParts.push('primaryKey')
|
|
78
|
+
gormParts.push(`type:${col.pgType}`)
|
|
79
|
+
if (!col.nullable) gormParts.push('not null')
|
|
80
|
+
if (col.isSerial) gormParts.push('autoIncrement')
|
|
81
|
+
if (col.defaultValue) gormParts.push(`default:${col.defaultValue}`)
|
|
82
|
+
if (col.foreignKey) {
|
|
83
|
+
gormParts.push(`foreignKey:${col.foreignKey.column}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const gormTag = `gorm:"${gormParts.join(';')}"`
|
|
87
|
+
const jsonTag = `json:"${col.name},omitempty"`
|
|
88
|
+
const tag = `\`${gormTag} ${jsonTag}\``
|
|
89
|
+
|
|
90
|
+
fields.push(`\t${col.pascalName} ${goType} ${tag}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
structBlocks.push(`type ${table.pascalName} struct {\n${fields.join('\n')}\n}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Views (read-only)
|
|
97
|
+
for (const view of schema.views.filter((v) => !v.skipped)) {
|
|
98
|
+
const fields: string[] = []
|
|
99
|
+
|
|
100
|
+
for (const col of view.columns) {
|
|
101
|
+
const goType = resolveGoType(col)
|
|
102
|
+
|
|
103
|
+
const gormTag = `gorm:"type:${col.pgType}"`
|
|
104
|
+
const jsonTag = `json:"${col.name},omitempty"`
|
|
105
|
+
const tag = `\`${gormTag} ${jsonTag}\``
|
|
106
|
+
|
|
107
|
+
fields.push(`\t${col.pascalName} ${goType} ${tag}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
structBlocks.push(
|
|
111
|
+
`// ${view.pascalName} is read-only (from view)\ntype ${view.pascalName} struct {\n${fields.join('\n')}\n}`,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let importBlock = ''
|
|
116
|
+
if (allImports.size > 0) {
|
|
117
|
+
const sorted = [...allImports].sort()
|
|
118
|
+
if (sorted.length === 1) {
|
|
119
|
+
importBlock = `import "${sorted[0]}"\n\n`
|
|
120
|
+
} else {
|
|
121
|
+
importBlock = `import (\n${sorted.map((i) => `\t"${i}"`).join('\n')}\n)\n\n`
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const content = `// Generated by @sqldoc/templates/gorm -- DO NOT EDIT
|
|
126
|
+
package models
|
|
127
|
+
|
|
128
|
+
${importBlock}${structBlocks.join('\n\n')}\n`
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
files: [{ path: 'models.go', content }],
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
FROM golang:1.24-alpine
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
# Set up Go module with models as a sub-package
|
|
4
|
+
RUN go mod init sqldoc-test
|
|
5
|
+
RUN mkdir -p models cmd/test
|
|
6
|
+
COPY models.go models/
|
|
7
|
+
COPY test.go cmd/test/main.go
|
|
8
|
+
ENV GOTOOLCHAIN=auto
|
|
9
|
+
RUN go get github.com/jackc/pgx/v5 gorm.io/gorm gorm.io/driver/postgres && go mod tidy
|
|
10
|
+
# Step 1: typecheck/compile all packages
|
|
11
|
+
RUN go build -o /usr/local/bin/test-runner ./cmd/test
|
|
12
|
+
# Step 2: run integration test against real DB
|
|
13
|
+
CMD ["test-runner"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
|
|
7
|
+
"sqldoc-test/models"
|
|
8
|
+
|
|
9
|
+
"gorm.io/driver/postgres"
|
|
10
|
+
"gorm.io/gorm"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
var failed int
|
|
14
|
+
|
|
15
|
+
func assert(condition bool, msg string) {
|
|
16
|
+
if !condition {
|
|
17
|
+
fmt.Fprintf(os.Stderr, "FAIL: %s\n", msg)
|
|
18
|
+
failed++
|
|
19
|
+
} else {
|
|
20
|
+
fmt.Printf(" ok: %s\n", msg)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func main() {
|
|
25
|
+
dbURL := os.Getenv("DATABASE_URL")
|
|
26
|
+
if dbURL == "" {
|
|
27
|
+
fmt.Fprintln(os.Stderr, "DATABASE_URL not set")
|
|
28
|
+
os.Exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
db, err := gorm.Open(postgres.Open(dbURL), &gorm.Config{})
|
|
32
|
+
if err != nil {
|
|
33
|
+
fmt.Fprintf(os.Stderr, "connect error: %v\n", err)
|
|
34
|
+
os.Exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fmt.Println("--- gorm integration test ---")
|
|
38
|
+
|
|
39
|
+
// 1. Query user via GORM using generated model
|
|
40
|
+
var user models.Users
|
|
41
|
+
if err := db.First(&user, 1).Error; err != nil {
|
|
42
|
+
fmt.Fprintf(os.Stderr, "query user error: %v\n", err)
|
|
43
|
+
os.Exit(1)
|
|
44
|
+
}
|
|
45
|
+
assert(user.Email == "test@example.com", "user.Email matches")
|
|
46
|
+
assert(user.Name != nil && *user.Name == "Test User", "user.Name matches")
|
|
47
|
+
assert(user.Age != nil && *user.Age == 30, "user.Age matches")
|
|
48
|
+
assert(user.IsActive == true, "user.IsActive matches")
|
|
49
|
+
|
|
50
|
+
// 2. Query post via GORM using generated model
|
|
51
|
+
var post models.Posts
|
|
52
|
+
if err := db.First(&post, 1).Error; err != nil {
|
|
53
|
+
fmt.Fprintf(os.Stderr, "query post error: %v\n", err)
|
|
54
|
+
os.Exit(1)
|
|
55
|
+
}
|
|
56
|
+
assert(post.Title == "Hello World", "post.Title matches")
|
|
57
|
+
assert(post.UserId == 1, "post.UserId matches")
|
|
58
|
+
assert(post.ViewCount == 42, "post.ViewCount matches")
|
|
59
|
+
|
|
60
|
+
if failed > 0 {
|
|
61
|
+
fmt.Fprintf(os.Stderr, "\n%d assertion(s) failed\n", failed)
|
|
62
|
+
os.Exit(1)
|
|
63
|
+
}
|
|
64
|
+
fmt.Println("\nAll assertions passed!")
|
|
65
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AtlasColumn, AtlasRealm, AtlasTable, AtlasView } from '@sqldoc/atlas'
|
|
2
|
+
|
|
3
|
+
/** Extract all tables from all schemas in a realm */
|
|
4
|
+
export function getTablesFromRealm(realm: AtlasRealm): AtlasTable[] {
|
|
5
|
+
return realm.schemas.flatMap((s) => s.tables ?? [])
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Extract all views from all schemas in a realm */
|
|
9
|
+
export function getViewsFromRealm(realm: AtlasRealm): AtlasView[] {
|
|
10
|
+
return realm.schemas.flatMap((s) => s.views ?? [])
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Check if a column is nullable */
|
|
14
|
+
export function isNullable(column: AtlasColumn): boolean {
|
|
15
|
+
return column.type?.null === true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Get the column type string, falling back to 'unknown' */
|
|
19
|
+
export function getColumnType(column: AtlasColumn): string {
|
|
20
|
+
return column.type?.T ?? column.type?.raw ?? 'unknown'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Find all codegen tags for a given SQL object from allFileTags */
|
|
24
|
+
export function findTagsForObject(
|
|
25
|
+
allFileTags: Array<{
|
|
26
|
+
sourceFile: string
|
|
27
|
+
objects: Array<{
|
|
28
|
+
objectName: string
|
|
29
|
+
target: string
|
|
30
|
+
tags: Array<{ namespace: string; tag: string | null; args: Record<string, unknown> | unknown[] }>
|
|
31
|
+
}>
|
|
32
|
+
}>,
|
|
33
|
+
objectName: string,
|
|
34
|
+
): Array<{ namespace: string; tag: string | null; args: Record<string, unknown> | unknown[] }> {
|
|
35
|
+
for (const file of allFileTags) {
|
|
36
|
+
for (const obj of file.objects) {
|
|
37
|
+
if (obj.objectName === objectName) {
|
|
38
|
+
return obj.tags
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return []
|
|
43
|
+
}
|