@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.
Files changed (108) hide show
  1. package/package.json +161 -0
  2. package/src/__tests__/dedent.test.ts +45 -0
  3. package/src/__tests__/docker-templates.test.ts +134 -0
  4. package/src/__tests__/go-structs.test.ts +184 -0
  5. package/src/__tests__/naming.test.ts +48 -0
  6. package/src/__tests__/python-dataclasses.test.ts +185 -0
  7. package/src/__tests__/rust-structs.test.ts +176 -0
  8. package/src/__tests__/tags-helpers.test.ts +72 -0
  9. package/src/__tests__/type-mapping.test.ts +332 -0
  10. package/src/__tests__/typescript.test.ts +202 -0
  11. package/src/cobol-copybook/index.ts +220 -0
  12. package/src/cobol-copybook/test/.gitignore +6 -0
  13. package/src/cobol-copybook/test/Dockerfile +7 -0
  14. package/src/csharp-records/index.ts +131 -0
  15. package/src/csharp-records/test/.gitignore +6 -0
  16. package/src/csharp-records/test/Dockerfile +6 -0
  17. package/src/diesel/index.ts +247 -0
  18. package/src/diesel/test/.gitignore +6 -0
  19. package/src/diesel/test/Dockerfile +16 -0
  20. package/src/drizzle/index.ts +255 -0
  21. package/src/drizzle/test/.gitignore +6 -0
  22. package/src/drizzle/test/Dockerfile +8 -0
  23. package/src/drizzle/test/test.ts +71 -0
  24. package/src/efcore/index.ts +190 -0
  25. package/src/efcore/test/.gitignore +6 -0
  26. package/src/efcore/test/Dockerfile +7 -0
  27. package/src/go-structs/index.ts +119 -0
  28. package/src/go-structs/test/.gitignore +6 -0
  29. package/src/go-structs/test/Dockerfile +13 -0
  30. package/src/go-structs/test/test.go +71 -0
  31. package/src/gorm/index.ts +134 -0
  32. package/src/gorm/test/.gitignore +6 -0
  33. package/src/gorm/test/Dockerfile +13 -0
  34. package/src/gorm/test/test.go +65 -0
  35. package/src/helpers/atlas.ts +43 -0
  36. package/src/helpers/enrich.ts +396 -0
  37. package/src/helpers/naming.ts +19 -0
  38. package/src/helpers/tags.ts +63 -0
  39. package/src/index.ts +24 -0
  40. package/src/java-records/index.ts +179 -0
  41. package/src/java-records/test/.gitignore +6 -0
  42. package/src/java-records/test/Dockerfile +11 -0
  43. package/src/java-records/test/Test.java +93 -0
  44. package/src/jpa/index.ts +279 -0
  45. package/src/jpa/test/.gitignore +6 -0
  46. package/src/jpa/test/Dockerfile +14 -0
  47. package/src/jpa/test/Test.java +111 -0
  48. package/src/json-schema/index.ts +351 -0
  49. package/src/json-schema/test/.gitignore +6 -0
  50. package/src/json-schema/test/Dockerfile +18 -0
  51. package/src/knex/index.ts +168 -0
  52. package/src/knex/test/.gitignore +6 -0
  53. package/src/knex/test/Dockerfile +7 -0
  54. package/src/knex/test/test.ts +75 -0
  55. package/src/kotlin-data/index.ts +147 -0
  56. package/src/kotlin-data/test/.gitignore +6 -0
  57. package/src/kotlin-data/test/Dockerfile +14 -0
  58. package/src/kotlin-data/test/Test.kt +82 -0
  59. package/src/kysely/index.ts +165 -0
  60. package/src/kysely/test/.gitignore +6 -0
  61. package/src/kysely/test/Dockerfile +8 -0
  62. package/src/kysely/test/test.ts +82 -0
  63. package/src/prisma/index.ts +387 -0
  64. package/src/prisma/test/.gitignore +6 -0
  65. package/src/prisma/test/Dockerfile +7 -0
  66. package/src/protobuf/index.ts +219 -0
  67. package/src/protobuf/test/.gitignore +6 -0
  68. package/src/protobuf/test/Dockerfile +6 -0
  69. package/src/pydantic/index.ts +272 -0
  70. package/src/pydantic/test/.gitignore +6 -0
  71. package/src/pydantic/test/Dockerfile +8 -0
  72. package/src/pydantic/test/test.py +63 -0
  73. package/src/python-dataclasses/index.ts +217 -0
  74. package/src/python-dataclasses/test/.gitignore +6 -0
  75. package/src/python-dataclasses/test/Dockerfile +8 -0
  76. package/src/python-dataclasses/test/test.py +63 -0
  77. package/src/rust-structs/index.ts +152 -0
  78. package/src/rust-structs/test/.gitignore +6 -0
  79. package/src/rust-structs/test/Dockerfile +22 -0
  80. package/src/rust-structs/test/test.rs +82 -0
  81. package/src/sqlalchemy/index.ts +258 -0
  82. package/src/sqlalchemy/test/.gitignore +6 -0
  83. package/src/sqlalchemy/test/Dockerfile +8 -0
  84. package/src/sqlalchemy/test/test.py +61 -0
  85. package/src/sqlc/index.ts +148 -0
  86. package/src/sqlc/test/.gitignore +6 -0
  87. package/src/sqlc/test/Dockerfile +13 -0
  88. package/src/sqlc/test/test.go +91 -0
  89. package/src/tags/dedent.ts +28 -0
  90. package/src/tags/index.ts +14 -0
  91. package/src/types/index.ts +8 -0
  92. package/src/types/pg-to-csharp.ts +136 -0
  93. package/src/types/pg-to-go.ts +120 -0
  94. package/src/types/pg-to-java.ts +141 -0
  95. package/src/types/pg-to-kotlin.ts +119 -0
  96. package/src/types/pg-to-python.ts +120 -0
  97. package/src/types/pg-to-rust.ts +121 -0
  98. package/src/types/pg-to-ts.ts +173 -0
  99. package/src/typescript/index.ts +168 -0
  100. package/src/typescript/test/.gitignore +6 -0
  101. package/src/typescript/test/Dockerfile +8 -0
  102. package/src/typescript/test/test.ts +89 -0
  103. package/src/xsd/index.ts +191 -0
  104. package/src/xsd/test/.gitignore +6 -0
  105. package/src/xsd/test/Dockerfile +6 -0
  106. package/src/zod/index.ts +289 -0
  107. package/src/zod/test/.gitignore +6 -0
  108. package/src/zod/test/Dockerfile +6 -0
@@ -0,0 +1,147 @@
1
+ import { defineTemplate } from '@sqldoc/ns-codegen'
2
+ import { activeTables, enrichRealm } from '../helpers/enrich.ts'
3
+ import { toCamelCase, toPascalCase, toScreamingSnake } from '../helpers/naming.ts'
4
+ import { pgToKotlin } from '../types/pg-to-kotlin.ts'
5
+
6
+ export default defineTemplate({
7
+ name: 'Kotlin Data Classes',
8
+ description: 'Generate Kotlin data classes from SQL schema',
9
+ language: 'kotlin',
10
+
11
+ generate(ctx) {
12
+ const schema = enrichRealm(ctx)
13
+ const classes: string[] = []
14
+
15
+ // Enums
16
+ for (const e of schema.enums) {
17
+ const className = toPascalCase(e.name)
18
+ const members = e.values.map((v) => ` ${toScreamingSnake(v)}`).join(',\n')
19
+ classes.push(`enum class ${className} {\n${members}\n}`)
20
+ }
21
+
22
+ // Composite types as data classes
23
+ const composites = new Map<string, Array<{ name: string; type: string }>>()
24
+ for (const table of schema.tables) {
25
+ for (const col of table.columns) {
26
+ if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
27
+ composites.set(col.pgType, col.compositeFields)
28
+ }
29
+ }
30
+ }
31
+ for (const [name, fields] of composites) {
32
+ const className = toPascalCase(name)
33
+ const ktFields = fields.map((f) => {
34
+ const ktType = pgToKotlin(f.type, false)
35
+ return ` val ${toCamelCase(f.name)}: ${ktType}`
36
+ })
37
+ classes.push(`data class ${className}(`)
38
+ classes.push(ktFields.join(',\n'))
39
+ classes.push(')')
40
+ }
41
+
42
+ for (const table of activeTables(schema)) {
43
+ const fields: string[] = []
44
+ for (const col of table.columns) {
45
+ let ktType: string
46
+ if (col.typeOverride) {
47
+ ktType = col.nullable ? `${col.typeOverride}?` : col.typeOverride
48
+ } else if (col.category === 'enum' && col.enumValues?.length) {
49
+ ktType = col.nullable ? `${toPascalCase(col.pgType)}?` : toPascalCase(col.pgType)
50
+ } else if (col.category === 'composite' && col.compositeFields?.length) {
51
+ const compositeType = toPascalCase(col.pgType)
52
+ ktType = col.nullable ? `${compositeType}?` : compositeType
53
+ } else {
54
+ ktType = pgToKotlin(col.pgType, col.nullable, col.category)
55
+ }
56
+
57
+ const defaultVal = col.nullable ? ' = null' : ''
58
+ fields.push(` val ${col.camelName}: ${ktType}${defaultVal}`)
59
+ }
60
+
61
+ classes.push(`data class ${table.pascalName}(`)
62
+ classes.push(fields.join(',\n'))
63
+ classes.push(')')
64
+ }
65
+
66
+ // Views (read-only — val only)
67
+ for (const view of schema.views.filter((v) => !v.skipped)) {
68
+ const fields: string[] = []
69
+ for (const col of view.columns) {
70
+ let ktType: string
71
+ if (col.typeOverride) {
72
+ ktType = col.nullable ? `${col.typeOverride}?` : col.typeOverride
73
+ } else if (col.category === 'enum' && col.enumValues?.length) {
74
+ ktType = col.nullable ? `${toPascalCase(col.pgType)}?` : toPascalCase(col.pgType)
75
+ } else if (col.category === 'composite' && col.compositeFields?.length) {
76
+ const compositeType = toPascalCase(col.pgType)
77
+ ktType = col.nullable ? `${compositeType}?` : compositeType
78
+ } else {
79
+ ktType = pgToKotlin(col.pgType, col.nullable, col.category)
80
+ }
81
+
82
+ const defaultVal = col.nullable ? ' = null' : ''
83
+ fields.push(` val ${col.camelName}: ${ktType}${defaultVal}`)
84
+ }
85
+
86
+ classes.push(`/** Read-only (from view) */`)
87
+ classes.push(`data class ${view.pascalName}(`)
88
+ classes.push(fields.join(',\n'))
89
+ classes.push(')')
90
+ }
91
+
92
+ // Functions (skip trigger functions)
93
+ for (const fn of schema.functions) {
94
+ const retRaw = fn.returnType?.type?.toLowerCase() ?? ''
95
+ if (retRaw === 'trigger') continue
96
+
97
+ const params = fn.args
98
+ .filter((a) => !a.name?.startsWith('_') && (a as any).mode !== 'OUT')
99
+ .map((a) => {
100
+ const argType = pgToKotlin(a.type, false, a.category)
101
+ return `${toCamelCase(a.name || 'arg')}: ${argType}`
102
+ })
103
+ .join(', ')
104
+
105
+ let retType: string
106
+ if (retRaw.startsWith('setof ')) {
107
+ const tableName = retRaw.replace('setof ', '')
108
+ const table = schema.tables.find((t) => t.name === tableName)
109
+ retType = table ? `List<${table.pascalName}>` : `List<${pgToKotlin(tableName, false)}>`
110
+ } else if (fn.returnType) {
111
+ retType = pgToKotlin(fn.returnType.type, false, fn.returnType.category)
112
+ } else {
113
+ retType = 'Unit'
114
+ }
115
+
116
+ classes.push(`typealias ${fn.pascalName} = (${params}) -> ${retType}`)
117
+ }
118
+
119
+ if (classes.length === 0) {
120
+ return { files: [] }
121
+ }
122
+
123
+ const imports = new Set<string>()
124
+ // Check if any imported types are used in generated content
125
+ const content = classes.join('\n')
126
+ if (content.includes('BigDecimal')) imports.add('import java.math.BigDecimal')
127
+ if (content.includes('OffsetDateTime')) imports.add('import java.time.OffsetDateTime')
128
+ if (content.includes('LocalDateTime')) imports.add('import java.time.LocalDateTime')
129
+ if (content.includes('LocalDate') && !content.includes('LocalDateTime')) imports.add('import java.time.LocalDate')
130
+ if (content.includes('LocalTime')) imports.add('import java.time.LocalTime')
131
+ if (content.includes('Duration')) imports.add('import java.time.Duration')
132
+ if (content.includes('UUID')) imports.add('import java.util.UUID')
133
+
134
+ const parts: string[] = []
135
+ const sortedImports = [...imports].sort()
136
+ if (sortedImports.length > 0) {
137
+ parts.push(sortedImports.join('\n'))
138
+ parts.push('')
139
+ }
140
+ parts.push(classes.join('\n\n'))
141
+ parts.push('')
142
+
143
+ return {
144
+ files: [{ path: 'Models.kt', content: parts.join('\n') }],
145
+ }
146
+ },
147
+ })
@@ -0,0 +1,6 @@
1
+ # Generated by codegen — only Dockerfile and test scripts are tracked
2
+ *
3
+ !.gitignore
4
+ !Dockerfile
5
+ !test.*
6
+ !Test.*
@@ -0,0 +1,14 @@
1
+ FROM eclipse-temurin:21-jdk-alpine
2
+ RUN apk add --no-cache curl unzip zip bash
3
+ # Install Kotlin via SDKMAN
4
+ RUN curl -s https://get.sdkman.io | bash && \
5
+ bash -c "source /root/.sdkman/bin/sdkman-init.sh && sdk install kotlin"
6
+ # Download PostgreSQL JDBC driver
7
+ RUN mkdir -p /deps && \
8
+ 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
9
+ WORKDIR /app
10
+ COPY . .
11
+ # Step 1: compile the generated data classes + test
12
+ RUN bash -c "source /root/.sdkman/bin/sdkman-init.sh && kotlinc -cp /deps/* *.kt Test.kt -include-runtime -d out.jar"
13
+ # Step 2: run integration test against real DB
14
+ CMD ["java", "-cp", "out.jar:/deps/*", "TestKt"]
@@ -0,0 +1,82 @@
1
+ import java.sql.DriverManager
2
+
3
+ /**
4
+ * Integration test for @sqldoc/templates/kotlin-data
5
+ * Connects to real Postgres via JDBC, verifies generated data classes work with actual data.
6
+ */
7
+ var failed = 0
8
+
9
+ fun assertEq(actual: Any?, expected: Any?, msg: String) {
10
+ if (actual != expected) {
11
+ System.err.println("FAIL: $msg (got $actual, expected $expected)")
12
+ failed++
13
+ } else {
14
+ println(" ok: $msg")
15
+ }
16
+ }
17
+
18
+ fun main() {
19
+ var dbUrl = System.getenv("DATABASE_URL")
20
+ if (dbUrl.isNullOrEmpty()) {
21
+ System.err.println("DATABASE_URL not set")
22
+ System.exit(1)
23
+ }
24
+
25
+ // Convert postgres(ql):// to jdbc:postgresql://, extracting userinfo for JDBC
26
+ val uri = java.net.URI(dbUrl.replaceFirst(Regex("^postgres(ql)?://"), "http://"))
27
+ val userInfo = uri.userInfo
28
+ var jdbcUrl = "jdbc:postgresql://${uri.host}:${if (uri.port > 0) uri.port else 5432}${uri.path}"
29
+ val query = uri.query
30
+ if (userInfo != null) {
31
+ val parts = userInfo.split(":", limit = 2)
32
+ val sep = if (query != null) "&" else "?"
33
+ jdbcUrl += (if (query != null) "?$query" else "") + "${sep}user=${parts[0]}&password=${parts.getOrElse(1) { "" }}"
34
+ } else if (query != null) {
35
+ jdbcUrl += "?$query"
36
+ }
37
+ dbUrl = jdbcUrl
38
+
39
+ println("--- kotlin-data integration test ---")
40
+
41
+ DriverManager.getConnection(dbUrl).use { conn ->
42
+ // 1. Query user and construct generated data class
43
+ conn.prepareStatement("SELECT id, email, name, age, is_active, created_at FROM users WHERE id = 1").use { ps ->
44
+ val rs = ps.executeQuery()
45
+ rs.next()
46
+ val user = Users(
47
+ id = rs.getLong("id"),
48
+ email = rs.getString("email"),
49
+ name = rs.getString("name"),
50
+ age = rs.getInt("age"),
51
+ isActive = rs.getBoolean("is_active"),
52
+ createdAt = rs.getObject("created_at", java.time.OffsetDateTime::class.java)
53
+ )
54
+ assertEq(user.email, "test@example.com", "user.email matches")
55
+ assertEq(user.name, "Test User", "user.name matches")
56
+ assertEq(user.age, 30, "user.age matches")
57
+ assertEq(user.isActive, true, "user.isActive matches")
58
+ }
59
+
60
+ // 2. Query post and construct generated data class
61
+ conn.prepareStatement("SELECT id, user_id, title, body, view_count FROM posts WHERE id = 1").use { ps ->
62
+ val rs = ps.executeQuery()
63
+ rs.next()
64
+ val post = Posts(
65
+ id = rs.getLong("id"),
66
+ userId = rs.getLong("user_id"),
67
+ title = rs.getString("title"),
68
+ body = rs.getString("body"),
69
+ viewCount = rs.getInt("view_count")
70
+ )
71
+ assertEq(post.title, "Hello World", "post.title matches")
72
+ assertEq(post.userId, 1L, "post.userId matches")
73
+ assertEq(post.viewCount, 42, "post.viewCount matches")
74
+ }
75
+ }
76
+
77
+ if (failed > 0) {
78
+ System.err.println("\n$failed assertion(s) failed")
79
+ System.exit(1)
80
+ }
81
+ println("\nAll assertions passed!")
82
+ }
@@ -0,0 +1,165 @@
1
+ import { defineTemplate } from '@sqldoc/ns-codegen'
2
+ import { activeTables, enrichRealm } from '../helpers/enrich.ts'
3
+ import { toCamelCase, toPascalCase } from '../helpers/naming.ts'
4
+ import { pgToTs, type TsTypeOptions } from '../types/pg-to-ts.ts'
5
+
6
+ export const configSchema = {
7
+ dateType: {
8
+ type: 'enum',
9
+ values: ['Date', 'dayjs', 'luxon', 'string'],
10
+ description: 'How to represent date/time types',
11
+ },
12
+ bigintType: {
13
+ type: 'enum',
14
+ values: ['number', 'bigint', 'string'],
15
+ description: 'How to represent bigint/bigserial columns',
16
+ },
17
+ } as const
18
+
19
+ export default defineTemplate({
20
+ name: 'Kysely Database',
21
+ description: 'Generate Kysely Database interface and table types from SQL schema',
22
+ language: 'typescript',
23
+ configSchema,
24
+
25
+ generate(ctx) {
26
+ const config = ctx.config ?? {}
27
+ const options: TsTypeOptions = {
28
+ dateType: config.dateType,
29
+ bigintType: config.bigintType,
30
+ nullableStyle: 'null-union',
31
+ }
32
+
33
+ const schema = enrichRealm(ctx)
34
+ const lines: string[] = [
35
+ '// Generated by @sqldoc/templates/kysely -- DO NOT EDIT',
36
+ '',
37
+ "import type { Generated } from 'kysely'",
38
+ '',
39
+ ]
40
+
41
+ // Enums
42
+ for (const e of schema.enums) {
43
+ const values = e.values.map((v) => `'${v}'`).join(' | ')
44
+ lines.push(`export type ${e.pascalName} = ${values}`)
45
+ lines.push('')
46
+ }
47
+
48
+ // Composite types (collected from columns)
49
+ const composites = new Map<string, Array<{ name: string; type: string }>>()
50
+ for (const table of schema.tables) {
51
+ for (const col of table.columns) {
52
+ if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
53
+ composites.set(col.pgType, col.compositeFields)
54
+ }
55
+ }
56
+ }
57
+ for (const [name, fields] of composites) {
58
+ const typeName = toPascalCase(name)
59
+ lines.push(`export interface ${typeName} {`)
60
+ for (const f of fields) {
61
+ lines.push(` ${toCamelCase(f.name)}: ${pgToTs(f.type, false, options)}`)
62
+ }
63
+ lines.push('}')
64
+ lines.push('')
65
+ }
66
+
67
+ // Generate per-table interfaces
68
+ const tableEntries: string[] = []
69
+
70
+ for (const table of activeTables(schema)) {
71
+ const interfaceName = `${table.pascalName}Table`
72
+
73
+ lines.push(`export interface ${interfaceName} {`)
74
+
75
+ for (const col of table.columns) {
76
+ let tsType = resolveType(col, options)
77
+
78
+ // Wrap auto-generated columns in Generated<T>
79
+ if (col.isSerial || col.raw.default !== undefined) {
80
+ tsType = `Generated<${tsType}>`
81
+ }
82
+
83
+ lines.push(` ${col.name}: ${tsType}`)
84
+ }
85
+
86
+ lines.push('}')
87
+ lines.push('')
88
+
89
+ tableEntries.push(` ${table.name}: ${interfaceName}`)
90
+ }
91
+
92
+ // Views (read-only)
93
+ for (const view of schema.views.filter((v) => !v.skipped)) {
94
+ const interfaceName = `${view.pascalName}View`
95
+
96
+ lines.push(`/** Read-only (from view) */`)
97
+ lines.push(`export interface ${interfaceName} {`)
98
+
99
+ for (const col of view.columns) {
100
+ const tsType = resolveType(col, options)
101
+ lines.push(` ${col.name}: ${tsType}`)
102
+ }
103
+
104
+ lines.push('}')
105
+ lines.push('')
106
+
107
+ tableEntries.push(` ${view.name}: ${interfaceName}`)
108
+ }
109
+
110
+ // Generate the Database interface
111
+ lines.push('export interface Database {')
112
+ lines.push(tableEntries.join('\n'))
113
+ lines.push('}')
114
+ lines.push('')
115
+
116
+ // Functions (skip trigger functions)
117
+ for (const fn of schema.functions) {
118
+ const retRaw = fn.returnType?.type?.toLowerCase() ?? ''
119
+ if (retRaw === 'trigger') continue
120
+
121
+ const params = fn.args
122
+ .filter((a) => !a.name?.startsWith('_') && (a as any).mode !== 'OUT')
123
+ .map((a) => {
124
+ const argType = pgToTs(a.type, false, options, a.category as any)
125
+ return `${toCamelCase(a.name || 'arg')}: ${argType}`
126
+ })
127
+ .join(', ')
128
+
129
+ let retType: string
130
+ if (retRaw.startsWith('setof ')) {
131
+ const tableName = retRaw.replace('setof ', '')
132
+ const table = schema.tables.find((t) => t.name === tableName)
133
+ retType = table ? `${table.pascalName}Table[]` : `${pgToTs(tableName, false, options)}[]`
134
+ } else if (fn.returnType) {
135
+ retType = pgToTs(fn.returnType.type, false, options, fn.returnType.category as any)
136
+ } else {
137
+ retType = 'void'
138
+ }
139
+
140
+ lines.push(`export type ${fn.pascalName} = (${params}) => ${retType}`)
141
+ lines.push('')
142
+ }
143
+
144
+ return {
145
+ files: [
146
+ {
147
+ path: 'database.ts',
148
+ content: lines.join('\n'),
149
+ },
150
+ ],
151
+ }
152
+
153
+ function resolveType(col: any, options: TsTypeOptions): string {
154
+ if (col.typeOverride) return col.typeOverride
155
+ if (col.category === 'enum' && col.enumValues?.length) {
156
+ return col.nullable ? `${toPascalCase(col.pgType)} | null` : toPascalCase(col.pgType)
157
+ }
158
+ if (col.category === 'composite' && col.compositeFields?.length) {
159
+ const compositeType = toPascalCase(col.pgType)
160
+ return col.nullable ? `${compositeType} | null` : compositeType
161
+ }
162
+ return pgToTs(col.pgType, col.nullable, options, col.category)
163
+ }
164
+ },
165
+ })
@@ -0,0 +1,6 @@
1
+ # Generated by codegen — only Dockerfile and test scripts are tracked
2
+ *
3
+ !.gitignore
4
+ !Dockerfile
5
+ !test.*
6
+ !Test.*
@@ -0,0 +1,8 @@
1
+ FROM node:23-slim
2
+ WORKDIR /app
3
+ COPY . .
4
+ RUN npm init -y && npm pkg set type=module && npm install typescript@5 kysely@0.27 pg @types/pg @types/node --save-dev
5
+ # Step 1: typecheck the generated types
6
+ RUN npx tsc --noEmit --strict --esModuleInterop --module nodenext --moduleResolution nodenext database.ts
7
+ # Step 2: run integration test against real DB
8
+ CMD ["node", "--experimental-strip-types", "test.ts"]
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Integration test for @sqldoc/templates/kysely
3
+ * Connects to real Postgres via Kysely, verifies generated Database interface works.
4
+ */
5
+ import { Kysely, PostgresDialect } from 'kysely'
6
+ import pg from 'pg'
7
+ import type { Database } from './database.ts'
8
+
9
+ const DATABASE_URL = process.env.DATABASE_URL
10
+ if (!DATABASE_URL) {
11
+ console.error('DATABASE_URL not set')
12
+ process.exit(1)
13
+ }
14
+
15
+ const db = new Kysely<Database>({
16
+ dialect: new PostgresDialect({
17
+ pool: new pg.Pool({ connectionString: DATABASE_URL }),
18
+ }),
19
+ })
20
+
21
+ let failed = 0
22
+ function assert(condition: boolean, msg: string) {
23
+ if (!condition) {
24
+ console.error(`FAIL: ${msg}`)
25
+ failed++
26
+ } else {
27
+ console.log(` ok: ${msg}`)
28
+ }
29
+ }
30
+
31
+ async function run() {
32
+ try {
33
+ console.log('--- kysely integration test ---')
34
+
35
+ // 1. Query known seeded user using Kysely's type-safe query builder
36
+ const user = await db.selectFrom('users').selectAll().where('id', '=', 1).executeTakeFirstOrThrow()
37
+
38
+ assert(user.email === 'test@example.com', 'user email matches')
39
+ assert(user.name === 'Test User', 'user name matches')
40
+ assert(user.is_active === true, 'user is_active matches')
41
+
42
+ // 2. Query known seeded post
43
+ const post = await db.selectFrom('posts').selectAll().where('id', '=', 1).executeTakeFirstOrThrow()
44
+
45
+ assert(post.title === 'Hello World', 'post title matches')
46
+
47
+ // 3. Insert a new post via type-safe insert
48
+ await db
49
+ .insertInto('posts')
50
+ .values({
51
+ user_id: 1,
52
+ title: 'Post from kysely',
53
+ body: 'test body',
54
+ view_count: 0,
55
+ })
56
+ .execute()
57
+
58
+ // 4. Read it back
59
+ const newPost = await db
60
+ .selectFrom('posts')
61
+ .selectAll()
62
+ .where('title', '=', 'Post from kysely')
63
+ .executeTakeFirstOrThrow()
64
+
65
+ assert(newPost.title === 'Post from kysely', 'inserted post title matches')
66
+ // pg returns bigint columns as strings; use loose equality for numeric comparison
67
+ assert(Number(newPost.user_id) === 1, 'inserted post user_id matches')
68
+
69
+ if (failed > 0) {
70
+ console.error(`\n${failed} assertion(s) failed`)
71
+ process.exit(1)
72
+ }
73
+ console.log('\nAll assertions passed!')
74
+ } finally {
75
+ await db.destroy()
76
+ }
77
+ }
78
+
79
+ run().catch((err) => {
80
+ console.error(err)
81
+ process.exit(1)
82
+ })