@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,93 @@
|
|
|
1
|
+
import java.sql.*;
|
|
2
|
+
import java.time.OffsetDateTime;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Integration test for @sqldoc/templates/java-records
|
|
6
|
+
* Connects to real Postgres, verifies generated records work with actual data.
|
|
7
|
+
*/
|
|
8
|
+
public class Test {
|
|
9
|
+
static int failed = 0;
|
|
10
|
+
|
|
11
|
+
static void assertEq(Object actual, Object expected, String msg) {
|
|
12
|
+
if (!actual.equals(expected)) {
|
|
13
|
+
System.err.printf("FAIL: %s (got %s, expected %s)%n", msg, actual, expected);
|
|
14
|
+
failed++;
|
|
15
|
+
} else {
|
|
16
|
+
System.out.printf(" ok: %s%n", msg);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public static void main(String[] args) throws Exception {
|
|
21
|
+
String dbUrl = System.getenv("DATABASE_URL");
|
|
22
|
+
if (dbUrl == null || dbUrl.isEmpty()) {
|
|
23
|
+
System.err.println("DATABASE_URL not set");
|
|
24
|
+
System.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convert postgres(ql):// to jdbc:postgresql://, extracting userinfo for JDBC
|
|
28
|
+
var uri = java.net.URI.create(dbUrl.replaceFirst("^postgres(ql)?://", "http://"));
|
|
29
|
+
var userInfo = uri.getUserInfo();
|
|
30
|
+
var jdbcUrl = "jdbc:postgresql://" + uri.getHost() + ":" + (uri.getPort() > 0 ? uri.getPort() : 5432) + uri.getPath();
|
|
31
|
+
var query = uri.getQuery();
|
|
32
|
+
if (userInfo != null) {
|
|
33
|
+
var parts = userInfo.split(":", 2);
|
|
34
|
+
var sep = query != null ? "&" : "?";
|
|
35
|
+
jdbcUrl += (query != null ? "?" + query : "") + sep + "user=" + parts[0] + "&password=" + (parts.length > 1 ? parts[1] : "");
|
|
36
|
+
} else if (query != null) {
|
|
37
|
+
jdbcUrl += "?" + query;
|
|
38
|
+
}
|
|
39
|
+
dbUrl = jdbcUrl;
|
|
40
|
+
|
|
41
|
+
System.out.println("--- java-records integration test ---");
|
|
42
|
+
|
|
43
|
+
try (Connection conn = DriverManager.getConnection(dbUrl)) {
|
|
44
|
+
// 1. Query user and construct generated record
|
|
45
|
+
try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = 1")) {
|
|
46
|
+
ResultSet rs = ps.executeQuery();
|
|
47
|
+
rs.next();
|
|
48
|
+
var user = new Users(
|
|
49
|
+
rs.getLong("id"),
|
|
50
|
+
rs.getString("email"),
|
|
51
|
+
rs.getString("name"),
|
|
52
|
+
rs.getObject("age") != null ? rs.getInt("age") : null,
|
|
53
|
+
rs.getBoolean("is_active"),
|
|
54
|
+
rs.getString("metadata"),
|
|
55
|
+
null, // address (composite)
|
|
56
|
+
rs.getObject("created_at", OffsetDateTime.class),
|
|
57
|
+
null, // tags (array)
|
|
58
|
+
rs.getBytes("avatar"),
|
|
59
|
+
rs.getBigDecimal("balance"),
|
|
60
|
+
rs.getObject("external_id") != null ? java.util.UUID.fromString(rs.getString("external_id")) : null
|
|
61
|
+
);
|
|
62
|
+
assertEq(user.email(), "test@example.com", "user.email() matches");
|
|
63
|
+
assertEq(user.name(), "Test User", "user.name() matches");
|
|
64
|
+
assertEq(user.age(), 30, "user.age() matches");
|
|
65
|
+
assertEq(user.isActive(), true, "user.isActive() matches");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Query post and construct generated record
|
|
69
|
+
try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM posts WHERE id = 1")) {
|
|
70
|
+
ResultSet rs = ps.executeQuery();
|
|
71
|
+
rs.next();
|
|
72
|
+
var post = new Posts(
|
|
73
|
+
rs.getLong("id"),
|
|
74
|
+
rs.getLong("user_id"),
|
|
75
|
+
rs.getString("title"),
|
|
76
|
+
rs.getString("body"),
|
|
77
|
+
rs.getObject("published_at") != null ? rs.getObject("published_at", OffsetDateTime.class) : null,
|
|
78
|
+
rs.getInt("view_count"),
|
|
79
|
+
rs.getObject("rating") != null ? rs.getDouble("rating") : null
|
|
80
|
+
);
|
|
81
|
+
assertEq(post.title(), "Hello World", "post.title() matches");
|
|
82
|
+
assertEq(post.userId(), 1L, "post.userId() matches");
|
|
83
|
+
assertEq(post.viewCount(), 42, "post.viewCount() matches");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (failed > 0) {
|
|
88
|
+
System.err.printf("%n%d assertion(s) failed%n", failed);
|
|
89
|
+
System.exit(1);
|
|
90
|
+
}
|
|
91
|
+
System.out.println("\nAll assertions passed!");
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/jpa/index.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { defineTemplate } from '@sqldoc/ns-codegen'
|
|
2
|
+
import { activeTables, enrichRealm, type TagEntry } from '../helpers/enrich.ts'
|
|
3
|
+
import { toCamelCase, toPascalCase, toScreamingSnake } from '../helpers/naming.ts'
|
|
4
|
+
import { pgToJava } from '../types/pg-to-java.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract varchar length from pgType, e.g. varchar(255) -> 255
|
|
8
|
+
*/
|
|
9
|
+
function getVarcharLength(pgType: string): number | undefined {
|
|
10
|
+
const match = pgType.match(/(?:varchar|character varying)\((\d+)\)/i)
|
|
11
|
+
return match ? parseInt(match[1], 10) : undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate validation annotations from @validate tags.
|
|
16
|
+
*/
|
|
17
|
+
function getValidationAnnotations(colTags: TagEntry[]): { annotations: string[]; imports: Set<string> } {
|
|
18
|
+
const annotations: string[] = []
|
|
19
|
+
const imports = new Set<string>()
|
|
20
|
+
|
|
21
|
+
for (const tag of colTags) {
|
|
22
|
+
if (tag.namespace !== 'validate') continue
|
|
23
|
+
|
|
24
|
+
if (tag.tag === 'notEmpty') {
|
|
25
|
+
annotations.push('@NotEmpty')
|
|
26
|
+
imports.add('jakarta.validation.constraints.NotEmpty')
|
|
27
|
+
} else if (tag.tag === 'length') {
|
|
28
|
+
const args = tag.args as Record<string, unknown>
|
|
29
|
+
const parts: string[] = []
|
|
30
|
+
if (args.min !== undefined) parts.push(`min = ${args.min}`)
|
|
31
|
+
if (args.max !== undefined) parts.push(`max = ${args.max}`)
|
|
32
|
+
annotations.push(`@Size(${parts.join(', ')})`)
|
|
33
|
+
imports.add('jakarta.validation.constraints.Size')
|
|
34
|
+
} else if (tag.tag === 'range') {
|
|
35
|
+
const args = tag.args as Record<string, unknown>
|
|
36
|
+
if (args.min !== undefined) {
|
|
37
|
+
annotations.push(`@Min(${args.min})`)
|
|
38
|
+
imports.add('jakarta.validation.constraints.Min')
|
|
39
|
+
}
|
|
40
|
+
if (args.max !== undefined) {
|
|
41
|
+
annotations.push(`@Max(${args.max})`)
|
|
42
|
+
imports.add('jakarta.validation.constraints.Max')
|
|
43
|
+
}
|
|
44
|
+
} else if (tag.tag === 'pattern') {
|
|
45
|
+
const pattern = Array.isArray(tag.args) ? tag.args[0] : undefined
|
|
46
|
+
if (pattern) {
|
|
47
|
+
// Escape backslashes for Java string literals
|
|
48
|
+
const escaped = pattern.replace(/\\/g, '\\\\')
|
|
49
|
+
annotations.push(`@Pattern(regexp = "${escaped}")`)
|
|
50
|
+
imports.add('jakarta.validation.constraints.Pattern')
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { annotations, imports }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default defineTemplate({
|
|
59
|
+
name: 'JPA Entities',
|
|
60
|
+
description: 'Generate JPA @Entity classes with annotations from SQL schema',
|
|
61
|
+
language: 'java',
|
|
62
|
+
|
|
63
|
+
generate(ctx) {
|
|
64
|
+
const schema = enrichRealm(ctx)
|
|
65
|
+
const files: Array<{ path: string; content: string }> = []
|
|
66
|
+
|
|
67
|
+
// Enums
|
|
68
|
+
for (const e of schema.enums) {
|
|
69
|
+
const className = toPascalCase(e.name)
|
|
70
|
+
const members = e.values.map((v) => {
|
|
71
|
+
const constName = toScreamingSnake(v)
|
|
72
|
+
return ` ${constName}("${v}")`
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const parts: string[] = []
|
|
76
|
+
parts.push(`public enum ${className} {`)
|
|
77
|
+
parts.push(`${members.join(',\n')};`)
|
|
78
|
+
parts.push('')
|
|
79
|
+
parts.push(` private final String value;`)
|
|
80
|
+
parts.push('')
|
|
81
|
+
parts.push(` ${className}(String value) {`)
|
|
82
|
+
parts.push(` this.value = value;`)
|
|
83
|
+
parts.push(` }`)
|
|
84
|
+
parts.push('')
|
|
85
|
+
parts.push(` public String getValue() {`)
|
|
86
|
+
parts.push(` return value;`)
|
|
87
|
+
parts.push(` }`)
|
|
88
|
+
parts.push('}')
|
|
89
|
+
parts.push('')
|
|
90
|
+
|
|
91
|
+
files.push({
|
|
92
|
+
path: `${className}.java`,
|
|
93
|
+
content: parts.join('\n'),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Composite types as @Embeddable classes
|
|
98
|
+
const composites = new Map<string, Array<{ name: string; type: string }>>()
|
|
99
|
+
for (const table of schema.tables) {
|
|
100
|
+
for (const col of table.columns) {
|
|
101
|
+
if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
|
|
102
|
+
composites.set(col.pgType, col.compositeFields)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const [name, fields] of composites) {
|
|
107
|
+
const className = toPascalCase(name)
|
|
108
|
+
const allImports = new Set<string>()
|
|
109
|
+
allImports.add('jakarta.persistence.Embeddable')
|
|
110
|
+
|
|
111
|
+
const fieldLines: string[] = []
|
|
112
|
+
for (const f of fields) {
|
|
113
|
+
const mapped = pgToJava(f.type, false)
|
|
114
|
+
for (const imp of mapped.imports) allImports.add(imp)
|
|
115
|
+
fieldLines.push(` private ${mapped.type} ${toCamelCase(f.name)};`)
|
|
116
|
+
fieldLines.push('')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const sortedImports = [...allImports].sort()
|
|
120
|
+
const importLines = sortedImports.map((imp) => `import ${imp};`)
|
|
121
|
+
|
|
122
|
+
const parts: string[] = []
|
|
123
|
+
parts.push(importLines.join('\n'))
|
|
124
|
+
parts.push('')
|
|
125
|
+
parts.push('@Embeddable')
|
|
126
|
+
parts.push(`public class ${className} {`)
|
|
127
|
+
parts.push('')
|
|
128
|
+
parts.push(fieldLines.join('\n'))
|
|
129
|
+
parts.push('}')
|
|
130
|
+
parts.push('')
|
|
131
|
+
|
|
132
|
+
files.push({
|
|
133
|
+
path: `${className}.java`,
|
|
134
|
+
content: parts.join('\n'),
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const table of activeTables(schema)) {
|
|
139
|
+
const allImports = new Set<string>()
|
|
140
|
+
allImports.add('jakarta.persistence.*')
|
|
141
|
+
|
|
142
|
+
const fieldLines: string[] = []
|
|
143
|
+
for (const col of table.columns) {
|
|
144
|
+
let javaType: string
|
|
145
|
+
if (col.typeOverride) {
|
|
146
|
+
javaType = col.typeOverride
|
|
147
|
+
} else if (col.category === 'enum' && col.enumValues?.length) {
|
|
148
|
+
javaType = toPascalCase(col.pgType)
|
|
149
|
+
} else if (col.category === 'composite' && col.compositeFields?.length) {
|
|
150
|
+
javaType = toPascalCase(col.pgType)
|
|
151
|
+
} else {
|
|
152
|
+
const mapped = pgToJava(col.pgType, col.nullable, col.category)
|
|
153
|
+
javaType = mapped.type
|
|
154
|
+
for (const imp of mapped.imports) allImports.add(imp)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const annotations: string[] = []
|
|
158
|
+
|
|
159
|
+
// PK annotations
|
|
160
|
+
if (col.isPrimaryKey) {
|
|
161
|
+
annotations.push(' @Id')
|
|
162
|
+
if (col.isSerial) {
|
|
163
|
+
annotations.push(' @GeneratedValue(strategy = GenerationType.IDENTITY)')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Enum annotation
|
|
168
|
+
if (col.category === 'enum' && col.enumValues?.length) {
|
|
169
|
+
annotations.push(' @Enumerated(EnumType.STRING)')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Composite annotation
|
|
173
|
+
if (col.category === 'composite' && col.compositeFields?.length) {
|
|
174
|
+
annotations.push(' @Embedded')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// FK annotations
|
|
178
|
+
if (col.foreignKey) {
|
|
179
|
+
annotations.push(` @ManyToOne`)
|
|
180
|
+
annotations.push(` @JoinColumn(name = "${col.name}")`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Column annotations
|
|
184
|
+
const colAnnotationParts: string[] = []
|
|
185
|
+
if (!col.nullable && !col.isPrimaryKey) {
|
|
186
|
+
colAnnotationParts.push('nullable = false')
|
|
187
|
+
}
|
|
188
|
+
const varcharLen = getVarcharLength(col.pgType)
|
|
189
|
+
if (varcharLen) {
|
|
190
|
+
colAnnotationParts.push(`length = ${varcharLen}`)
|
|
191
|
+
}
|
|
192
|
+
if (colAnnotationParts.length > 0) {
|
|
193
|
+
annotations.push(` @Column(${colAnnotationParts.join(', ')})`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validation annotations from @validate tags
|
|
197
|
+
const validation = getValidationAnnotations(col.tags)
|
|
198
|
+
for (const ann of validation.annotations) {
|
|
199
|
+
annotations.push(` ${ann}`)
|
|
200
|
+
}
|
|
201
|
+
for (const imp of validation.imports) {
|
|
202
|
+
allImports.add(imp)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (annotations.length > 0) {
|
|
206
|
+
fieldLines.push(annotations.join('\n'))
|
|
207
|
+
}
|
|
208
|
+
fieldLines.push(` private ${javaType} ${col.camelName};`)
|
|
209
|
+
fieldLines.push('')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const sortedImports = [...allImports].sort()
|
|
213
|
+
const importLines = sortedImports.map((imp) => `import ${imp};`)
|
|
214
|
+
|
|
215
|
+
const parts: string[] = []
|
|
216
|
+
parts.push(importLines.join('\n'))
|
|
217
|
+
parts.push('')
|
|
218
|
+
parts.push('@Entity')
|
|
219
|
+
parts.push(`@Table(name = "${table.name}")`)
|
|
220
|
+
parts.push(`public class ${table.pascalName} {`)
|
|
221
|
+
parts.push('')
|
|
222
|
+
parts.push(fieldLines.join('\n'))
|
|
223
|
+
parts.push('}')
|
|
224
|
+
parts.push('')
|
|
225
|
+
|
|
226
|
+
files.push({
|
|
227
|
+
path: `${table.pascalName}.java`,
|
|
228
|
+
content: parts.join('\n'),
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Views (read-only — mapped as entities with a comment noting immutability)
|
|
233
|
+
for (const view of schema.views.filter((v) => !v.skipped)) {
|
|
234
|
+
const allImports = new Set<string>()
|
|
235
|
+
allImports.add('jakarta.persistence.*')
|
|
236
|
+
|
|
237
|
+
const fieldLines: string[] = []
|
|
238
|
+
for (const col of view.columns) {
|
|
239
|
+
let javaType: string
|
|
240
|
+
if (col.typeOverride) {
|
|
241
|
+
javaType = col.typeOverride
|
|
242
|
+
} else if (col.category === 'enum' && col.enumValues?.length) {
|
|
243
|
+
javaType = toPascalCase(col.pgType)
|
|
244
|
+
} else if (col.category === 'composite' && col.compositeFields?.length) {
|
|
245
|
+
javaType = toPascalCase(col.pgType)
|
|
246
|
+
} else {
|
|
247
|
+
const mapped = pgToJava(col.pgType, col.nullable, col.category)
|
|
248
|
+
javaType = mapped.type
|
|
249
|
+
for (const imp of mapped.imports) allImports.add(imp)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fieldLines.push(` private ${javaType} ${col.camelName};`)
|
|
253
|
+
fieldLines.push('')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const sortedImports = [...allImports].sort()
|
|
257
|
+
const importLines = sortedImports.map((imp) => `import ${imp};`)
|
|
258
|
+
|
|
259
|
+
const parts: string[] = []
|
|
260
|
+
parts.push(importLines.join('\n'))
|
|
261
|
+
parts.push('')
|
|
262
|
+
parts.push('/** Read-only (from view) */')
|
|
263
|
+
parts.push('@Entity')
|
|
264
|
+
parts.push(`@Table(name = "${view.name}")`)
|
|
265
|
+
parts.push(`public class ${view.pascalName} {`)
|
|
266
|
+
parts.push('')
|
|
267
|
+
parts.push(fieldLines.join('\n'))
|
|
268
|
+
parts.push('}')
|
|
269
|
+
parts.push('')
|
|
270
|
+
|
|
271
|
+
files.push({
|
|
272
|
+
path: `${view.pascalName}.java`,
|
|
273
|
+
content: parts.join('\n'),
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { files }
|
|
278
|
+
},
|
|
279
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FROM eclipse-temurin:21-jdk-alpine
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
RUN apk add --no-cache curl
|
|
4
|
+
# Download Jakarta Persistence API JARs and PostgreSQL JDBC driver
|
|
5
|
+
RUN mkdir -p /deps && \
|
|
6
|
+
curl -sL -o /deps/jakarta.persistence-api-3.2.0.jar https://repo1.maven.org/maven2/jakarta/persistence/jakarta.persistence-api/3.2.0/jakarta.persistence-api-3.2.0.jar && \
|
|
7
|
+
curl -sL -o /deps/jakarta.validation-api-3.1.0.jar https://repo1.maven.org/maven2/jakarta/validation/jakarta.validation-api/3.1.0/jakarta.validation-api-3.1.0.jar && \
|
|
8
|
+
curl -sL -o /deps/jakarta.annotation-api-3.0.0.jar https://repo1.maven.org/maven2/jakarta/annotation/jakarta.annotation-api/3.0.0/jakarta.annotation-api-3.0.0.jar && \
|
|
9
|
+
curl -sL -o /deps/postgresql-42.7.4.jar https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.4/postgresql-42.7.4.jar
|
|
10
|
+
COPY . .
|
|
11
|
+
# Step 1: compile the generated JPA entities + test
|
|
12
|
+
RUN javac -cp "/deps/*:." *.java Test.java
|
|
13
|
+
# Step 2: run integration test against real DB
|
|
14
|
+
CMD ["java", "-cp", "/deps/*:.", "Test"]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import java.sql.*;
|
|
2
|
+
import java.lang.reflect.Field;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Integration test for @sqldoc/templates/jpa
|
|
6
|
+
* Verifies generated JPA entities compile, have expected fields, and DB has expected data.
|
|
7
|
+
*/
|
|
8
|
+
public class Test {
|
|
9
|
+
static int failed = 0;
|
|
10
|
+
|
|
11
|
+
static void assertEq(Object actual, Object expected, String msg) {
|
|
12
|
+
if (!actual.equals(expected)) {
|
|
13
|
+
System.err.printf("FAIL: %s (got %s, expected %s)%n", msg, actual, expected);
|
|
14
|
+
failed++;
|
|
15
|
+
} else {
|
|
16
|
+
System.out.printf(" ok: %s%n", msg);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static void assertHasField(Class<?> cls, String fieldName, String msg) {
|
|
21
|
+
try {
|
|
22
|
+
cls.getDeclaredField(fieldName);
|
|
23
|
+
System.out.printf(" ok: %s%n", msg);
|
|
24
|
+
} catch (NoSuchFieldException e) {
|
|
25
|
+
System.err.printf("FAIL: %s (field '%s' not found)%n", msg, fieldName);
|
|
26
|
+
failed++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static void main(String[] args) throws Exception {
|
|
31
|
+
String dbUrl = System.getenv("DATABASE_URL");
|
|
32
|
+
if (dbUrl == null || dbUrl.isEmpty()) {
|
|
33
|
+
System.err.println("DATABASE_URL not set");
|
|
34
|
+
System.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Convert postgres(ql):// to jdbc:postgresql://, extracting userinfo for JDBC
|
|
38
|
+
var uri = java.net.URI.create(dbUrl.replaceFirst("^postgres(ql)?://", "http://"));
|
|
39
|
+
var userInfo = uri.getUserInfo();
|
|
40
|
+
var jdbcUrl = "jdbc:postgresql://" + uri.getHost() + ":" + (uri.getPort() > 0 ? uri.getPort() : 5432) + uri.getPath();
|
|
41
|
+
var query = uri.getQuery();
|
|
42
|
+
if (userInfo != null) {
|
|
43
|
+
var parts = userInfo.split(":", 2);
|
|
44
|
+
var sep = query != null ? "&" : "?";
|
|
45
|
+
jdbcUrl += (query != null ? "?" + query : "") + sep + "user=" + parts[0] + "&password=" + (parts.length > 1 ? parts[1] : "");
|
|
46
|
+
} else if (query != null) {
|
|
47
|
+
jdbcUrl += "?" + query;
|
|
48
|
+
}
|
|
49
|
+
dbUrl = jdbcUrl;
|
|
50
|
+
|
|
51
|
+
System.out.println("--- jpa integration test ---");
|
|
52
|
+
|
|
53
|
+
// 1. Verify generated entity classes have expected fields
|
|
54
|
+
assertHasField(Users.class, "id", "Users has 'id' field");
|
|
55
|
+
assertHasField(Users.class, "email", "Users has 'email' field");
|
|
56
|
+
assertHasField(Users.class, "name", "Users has 'name' field");
|
|
57
|
+
assertHasField(Users.class, "isActive", "Users has 'isActive' field");
|
|
58
|
+
assertHasField(Posts.class, "title", "Posts has 'title' field");
|
|
59
|
+
assertHasField(Posts.class, "viewCount", "Posts has 'viewCount' field");
|
|
60
|
+
|
|
61
|
+
// 2. Instantiate entity and populate via reflection (JPA entities have private fields)
|
|
62
|
+
try (Connection conn = DriverManager.getConnection(dbUrl)) {
|
|
63
|
+
try (PreparedStatement ps = conn.prepareStatement(
|
|
64
|
+
"SELECT id, email, name, age, is_active FROM users WHERE id = 1")) {
|
|
65
|
+
ResultSet rs = ps.executeQuery();
|
|
66
|
+
rs.next();
|
|
67
|
+
var user = new Users();
|
|
68
|
+
setField(user, "id", rs.getLong("id"));
|
|
69
|
+
setField(user, "email", rs.getString("email"));
|
|
70
|
+
setField(user, "name", rs.getString("name"));
|
|
71
|
+
setField(user, "age", rs.getObject("age") != null ? rs.getInt("age") : null);
|
|
72
|
+
setField(user, "isActive", rs.getBoolean("is_active"));
|
|
73
|
+
|
|
74
|
+
assertEq(getField(user, "email"), "test@example.com", "user.email matches");
|
|
75
|
+
assertEq(getField(user, "name"), "Test User", "user.name matches");
|
|
76
|
+
assertEq(getField(user, "isActive"), true, "user.isActive matches");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try (PreparedStatement ps = conn.prepareStatement(
|
|
80
|
+
"SELECT id, title, view_count FROM posts WHERE id = 1")) {
|
|
81
|
+
ResultSet rs = ps.executeQuery();
|
|
82
|
+
rs.next();
|
|
83
|
+
var post = new Posts();
|
|
84
|
+
setField(post, "id", rs.getLong("id"));
|
|
85
|
+
setField(post, "title", rs.getString("title"));
|
|
86
|
+
setField(post, "viewCount", rs.getInt("view_count"));
|
|
87
|
+
|
|
88
|
+
assertEq(getField(post, "title"), "Hello World", "post.title matches");
|
|
89
|
+
assertEq(getField(post, "viewCount"), 42, "post.viewCount matches");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (failed > 0) {
|
|
94
|
+
System.err.printf("%n%d assertion(s) failed%n", failed);
|
|
95
|
+
System.exit(1);
|
|
96
|
+
}
|
|
97
|
+
System.out.println("\nAll assertions passed!");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static void setField(Object obj, String name, Object value) throws Exception {
|
|
101
|
+
Field f = obj.getClass().getDeclaredField(name);
|
|
102
|
+
f.setAccessible(true);
|
|
103
|
+
f.set(obj, value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static Object getField(Object obj, String name) throws Exception {
|
|
107
|
+
Field f = obj.getClass().getDeclaredField(name);
|
|
108
|
+
f.setAccessible(true);
|
|
109
|
+
return f.get(obj);
|
|
110
|
+
}
|
|
111
|
+
}
|