@sqldoc/templates 0.0.1 → 0.0.2
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 +10 -8
- package/src/__tests__/dedent.test.ts +0 -45
- package/src/__tests__/docker-templates.test.ts +0 -134
- package/src/__tests__/go-structs.test.ts +0 -184
- package/src/__tests__/naming.test.ts +0 -48
- package/src/__tests__/python-dataclasses.test.ts +0 -185
- package/src/__tests__/rust-structs.test.ts +0 -176
- package/src/__tests__/tags-helpers.test.ts +0 -72
- package/src/__tests__/type-mapping.test.ts +0 -332
- package/src/__tests__/typescript.test.ts +0 -202
- package/src/cobol-copybook/test/.gitignore +0 -6
- package/src/cobol-copybook/test/Dockerfile +0 -7
- package/src/csharp-records/test/.gitignore +0 -6
- package/src/csharp-records/test/Dockerfile +0 -6
- package/src/diesel/test/.gitignore +0 -6
- package/src/diesel/test/Dockerfile +0 -16
- package/src/drizzle/test/.gitignore +0 -6
- package/src/drizzle/test/Dockerfile +0 -8
- package/src/drizzle/test/test.ts +0 -71
- package/src/efcore/test/.gitignore +0 -6
- package/src/efcore/test/Dockerfile +0 -7
- package/src/go-structs/test/.gitignore +0 -6
- package/src/go-structs/test/Dockerfile +0 -13
- package/src/go-structs/test/test.go +0 -71
- package/src/gorm/test/.gitignore +0 -6
- package/src/gorm/test/Dockerfile +0 -13
- package/src/gorm/test/test.go +0 -65
- package/src/java-records/test/.gitignore +0 -6
- package/src/java-records/test/Dockerfile +0 -11
- package/src/java-records/test/Test.java +0 -93
- package/src/jpa/test/.gitignore +0 -6
- package/src/jpa/test/Dockerfile +0 -14
- package/src/jpa/test/Test.java +0 -111
- package/src/json-schema/test/.gitignore +0 -6
- package/src/json-schema/test/Dockerfile +0 -18
- package/src/knex/test/.gitignore +0 -6
- package/src/knex/test/Dockerfile +0 -7
- package/src/knex/test/test.ts +0 -75
- package/src/kotlin-data/test/.gitignore +0 -6
- package/src/kotlin-data/test/Dockerfile +0 -14
- package/src/kotlin-data/test/Test.kt +0 -82
- package/src/kysely/test/.gitignore +0 -6
- package/src/kysely/test/Dockerfile +0 -8
- package/src/kysely/test/test.ts +0 -82
- package/src/prisma/test/.gitignore +0 -6
- package/src/prisma/test/Dockerfile +0 -7
- package/src/protobuf/test/.gitignore +0 -6
- package/src/protobuf/test/Dockerfile +0 -6
- package/src/pydantic/test/.gitignore +0 -6
- package/src/pydantic/test/Dockerfile +0 -8
- package/src/pydantic/test/test.py +0 -63
- package/src/python-dataclasses/test/.gitignore +0 -6
- package/src/python-dataclasses/test/Dockerfile +0 -8
- package/src/python-dataclasses/test/test.py +0 -63
- package/src/rust-structs/test/.gitignore +0 -6
- package/src/rust-structs/test/Dockerfile +0 -22
- package/src/rust-structs/test/test.rs +0 -82
- package/src/sqlalchemy/test/.gitignore +0 -6
- package/src/sqlalchemy/test/Dockerfile +0 -8
- package/src/sqlalchemy/test/test.py +0 -61
- package/src/sqlc/test/.gitignore +0 -6
- package/src/sqlc/test/Dockerfile +0 -13
- package/src/sqlc/test/test.go +0 -91
- package/src/typescript/test/.gitignore +0 -6
- package/src/typescript/test/Dockerfile +0 -8
- package/src/typescript/test/test.ts +0 -89
- package/src/xsd/test/.gitignore +0 -6
- package/src/xsd/test/Dockerfile +0 -6
- package/src/zod/test/.gitignore +0 -6
- package/src/zod/test/Dockerfile +0 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sqldoc/templates",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.2",
|
|
5
5
|
"description": "Code generation templates for sqldoc -- 18 templates across 6 languages plus tagged template syntax helpers",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -134,12 +134,14 @@
|
|
|
134
134
|
"types": "./src/index.ts",
|
|
135
135
|
"files": [
|
|
136
136
|
"src",
|
|
137
|
+
"!src/__tests__",
|
|
138
|
+
"!src/**/test",
|
|
137
139
|
"package.json"
|
|
138
140
|
],
|
|
139
141
|
"peerDependencies": {
|
|
140
|
-
"@sqldoc/atlas": "0.0.
|
|
141
|
-
"@sqldoc/
|
|
142
|
-
"@sqldoc/
|
|
142
|
+
"@sqldoc/atlas": "0.0.2",
|
|
143
|
+
"@sqldoc/ns-codegen": "0.0.2",
|
|
144
|
+
"@sqldoc/core": "0.0.2"
|
|
143
145
|
},
|
|
144
146
|
"devDependencies": {
|
|
145
147
|
"@electric-sql/pglite": "^0.4.1",
|
|
@@ -149,10 +151,10 @@
|
|
|
149
151
|
"tsx": "^4.21.0",
|
|
150
152
|
"typescript": "^5.9.3",
|
|
151
153
|
"vitest": "^4.1.0",
|
|
152
|
-
"@sqldoc/
|
|
153
|
-
"@sqldoc/
|
|
154
|
-
"@sqldoc/
|
|
155
|
-
"@sqldoc/ns-validate": "0.0.
|
|
154
|
+
"@sqldoc/core": "0.0.2",
|
|
155
|
+
"@sqldoc/ns-codegen": "0.0.2",
|
|
156
|
+
"@sqldoc/atlas": "0.0.2",
|
|
157
|
+
"@sqldoc/ns-validate": "0.0.2"
|
|
156
158
|
},
|
|
157
159
|
"scripts": {
|
|
158
160
|
"test": "vitest run",
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { dedent } from '../tags/dedent'
|
|
3
|
-
|
|
4
|
-
describe('dedent', () => {
|
|
5
|
-
it('strips common leading whitespace', () => {
|
|
6
|
-
const result = dedent`
|
|
7
|
-
hello
|
|
8
|
-
world
|
|
9
|
-
`
|
|
10
|
-
expect(result).toBe('hello\nworld')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('preserves relative indentation', () => {
|
|
14
|
-
const result = dedent`
|
|
15
|
-
if (true) {
|
|
16
|
-
return 1
|
|
17
|
-
}
|
|
18
|
-
`
|
|
19
|
-
expect(result).toBe('if (true) {\n return 1\n}')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('handles string interpolation', () => {
|
|
23
|
-
const name = 'World'
|
|
24
|
-
const result = dedent`
|
|
25
|
-
hello ${name}
|
|
26
|
-
goodbye
|
|
27
|
-
`
|
|
28
|
-
expect(result).toBe('hello World\ngoodbye')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('handles zero-indent lines', () => {
|
|
32
|
-
const result = dedent`
|
|
33
|
-
hello
|
|
34
|
-
world
|
|
35
|
-
`
|
|
36
|
-
expect(result).toBe('hello\nworld')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('removes empty first and last lines', () => {
|
|
40
|
-
const result = dedent`
|
|
41
|
-
content
|
|
42
|
-
`
|
|
43
|
-
expect(result).toBe('content')
|
|
44
|
-
})
|
|
45
|
-
})
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Docker-based template tests.
|
|
3
|
-
*
|
|
4
|
-
* Auto-discovers templates by finding src/<template>/test/Dockerfile.
|
|
5
|
-
* Starts a Postgres container on a shared Docker network, seeds it,
|
|
6
|
-
* runs codegen (output goes directly into each test dir), then for each template:
|
|
7
|
-
* 1. docker build (typecheck / compile)
|
|
8
|
-
* 2. docker run (integration test or no-op CMD)
|
|
9
|
-
*
|
|
10
|
-
* Run with: pnpm test:docker
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { execSync } from 'node:child_process'
|
|
14
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
15
|
-
import { dirname, join, resolve } from 'node:path'
|
|
16
|
-
import { fileURLToPath } from 'node:url'
|
|
17
|
-
import pg from 'pg'
|
|
18
|
-
import { afterAll, beforeAll, describe, test } from 'vitest'
|
|
19
|
-
|
|
20
|
-
const thisDir = dirname(fileURLToPath(import.meta.url))
|
|
21
|
-
const TEMPLATES_DIR = resolve(thisDir, '../..')
|
|
22
|
-
const SRC_DIR = join(TEMPLATES_DIR, 'src')
|
|
23
|
-
const TEST_DIR = join(TEMPLATES_DIR, 'test')
|
|
24
|
-
const REPO_ROOT = resolve(TEMPLATES_DIR, '../..')
|
|
25
|
-
|
|
26
|
-
const NETWORK = 'sqldoc-test-net'
|
|
27
|
-
const PG_CONTAINER = 'sqldoc-test-pg'
|
|
28
|
-
const PG_PORT = 54321
|
|
29
|
-
const DB_URL = `postgresql://postgres:postgres@${PG_CONTAINER}:5432/postgres`
|
|
30
|
-
|
|
31
|
-
function discoverTemplates(): string[] {
|
|
32
|
-
return readdirSync(SRC_DIR, { withFileTypes: true })
|
|
33
|
-
.filter((d) => d.isDirectory() && existsSync(join(SRC_DIR, d.name, 'test', 'Dockerfile')))
|
|
34
|
-
.map((d) => d.name)
|
|
35
|
-
.sort()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const templates = discoverTemplates()
|
|
39
|
-
|
|
40
|
-
function dockerExec(cmd: string, timeout = 30_000) {
|
|
41
|
-
return execSync(cmd, { stdio: 'pipe', timeout }).toString()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
beforeAll(async () => {
|
|
45
|
-
// Clean up any leftovers from previous runs, then create fresh
|
|
46
|
-
try {
|
|
47
|
-
dockerExec(`docker rm -f ${PG_CONTAINER}`)
|
|
48
|
-
} catch {}
|
|
49
|
-
try {
|
|
50
|
-
dockerExec(`docker network rm ${NETWORK}`)
|
|
51
|
-
} catch {}
|
|
52
|
-
dockerExec(`docker network create ${NETWORK}`)
|
|
53
|
-
dockerExec(
|
|
54
|
-
`docker run -d --name ${PG_CONTAINER} --network ${NETWORK} -p ${PG_PORT}:5432 -e POSTGRES_PASSWORD=postgres postgres:17-alpine`,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
// Wait for postgres to be ready
|
|
58
|
-
for (let i = 0; i < 30; i++) {
|
|
59
|
-
try {
|
|
60
|
-
dockerExec(`docker exec ${PG_CONTAINER} pg_isready -U postgres`)
|
|
61
|
-
break
|
|
62
|
-
} catch {
|
|
63
|
-
await new Promise((r) => setTimeout(r, 1000))
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Seed the database
|
|
68
|
-
const client = new pg.Client({
|
|
69
|
-
host: '127.0.0.1',
|
|
70
|
-
port: PG_PORT,
|
|
71
|
-
database: 'postgres',
|
|
72
|
-
user: 'postgres',
|
|
73
|
-
password: 'postgres',
|
|
74
|
-
connectionTimeoutMillis: 10_000,
|
|
75
|
-
})
|
|
76
|
-
await client.connect()
|
|
77
|
-
const schemaSql = readFileSync(join(TEST_DIR, 'fixture.sql'), 'utf-8')
|
|
78
|
-
.split('\n')
|
|
79
|
-
.filter((line) => !line.trim().startsWith('-- @import'))
|
|
80
|
-
.join('\n')
|
|
81
|
-
await client.query(schemaSql)
|
|
82
|
-
await client.query(readFileSync(join(TEST_DIR, 'fixture-seed.sql'), 'utf-8'))
|
|
83
|
-
await client.end()
|
|
84
|
-
console.log('Database seeded.')
|
|
85
|
-
|
|
86
|
-
// Run codegen — outputs directly into each src/<template>/test/ directory
|
|
87
|
-
console.log('Running codegen...')
|
|
88
|
-
execSync(
|
|
89
|
-
`node ${join(REPO_ROOT, 'packages/cli/src/index.ts')} codegen -c ${join(TEST_DIR, 'sqldoc.config.ts')} ${join(TEST_DIR, 'fixture.sql')}`,
|
|
90
|
-
{
|
|
91
|
-
cwd: TEMPLATES_DIR,
|
|
92
|
-
stdio: 'pipe',
|
|
93
|
-
timeout: 120_000,
|
|
94
|
-
env: { ...process.env, SQLDOC_RESOLVE_FROM_LOCAL_PACKAGE: 'true' },
|
|
95
|
-
},
|
|
96
|
-
)
|
|
97
|
-
console.log(`Codegen complete. Testing ${templates.length} templates.`)
|
|
98
|
-
}, 180_000)
|
|
99
|
-
|
|
100
|
-
afterAll(() => {
|
|
101
|
-
try {
|
|
102
|
-
dockerExec(`docker rm -f ${PG_CONTAINER}`)
|
|
103
|
-
} catch {}
|
|
104
|
-
try {
|
|
105
|
-
dockerExec(`docker network rm ${NETWORK}`)
|
|
106
|
-
} catch {}
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
describe('docker templates', () => {
|
|
110
|
-
for (const name of templates) {
|
|
111
|
-
test(name, () => {
|
|
112
|
-
const testDir = join(SRC_DIR, name, 'test')
|
|
113
|
-
const tag = `sqldoc-test-${name}`
|
|
114
|
-
try {
|
|
115
|
-
execSync(`docker build -t ${tag} ${testDir}`, {
|
|
116
|
-
stdio: 'pipe',
|
|
117
|
-
timeout: 120_000,
|
|
118
|
-
})
|
|
119
|
-
execSync(`docker run --rm --network ${NETWORK} -e DATABASE_URL="${DB_URL}" ${tag}`, {
|
|
120
|
-
stdio: 'pipe',
|
|
121
|
-
timeout: 30_000,
|
|
122
|
-
})
|
|
123
|
-
} catch (err: any) {
|
|
124
|
-
const stderr = err.stderr?.toString() ?? ''
|
|
125
|
-
const stdout = err.stdout?.toString() ?? ''
|
|
126
|
-
throw new Error(`Docker test failed for ${name}:\n${stderr}\n${stdout}`)
|
|
127
|
-
} finally {
|
|
128
|
-
try {
|
|
129
|
-
execSync(`docker rmi ${tag}`, { stdio: 'ignore' })
|
|
130
|
-
} catch {}
|
|
131
|
-
}
|
|
132
|
-
}, 180_000)
|
|
133
|
-
}
|
|
134
|
-
})
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import goStructs from '../go-structs/index.ts'
|
|
3
|
-
|
|
4
|
-
const generate = goStructs.generate
|
|
5
|
-
|
|
6
|
-
import type { AtlasRealm } from '@sqldoc/atlas'
|
|
7
|
-
import type { TemplateContext } from '@sqldoc/ns-codegen'
|
|
8
|
-
|
|
9
|
-
const testRealm: AtlasRealm = {
|
|
10
|
-
schemas: [
|
|
11
|
-
{
|
|
12
|
-
name: 'public',
|
|
13
|
-
tables: [
|
|
14
|
-
{
|
|
15
|
-
name: 'users',
|
|
16
|
-
columns: [
|
|
17
|
-
{ name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
|
|
18
|
-
{ name: 'email', type: { T: 'character varying', raw: 'varchar(255)', null: false, category: 'string' } },
|
|
19
|
-
{ name: 'name', type: { T: 'text', null: true, category: 'string' } },
|
|
20
|
-
{ name: 'age', type: { T: 'integer', null: true, category: 'integer' } },
|
|
21
|
-
{ name: 'is_active', type: { T: 'boolean', null: false, category: 'boolean' } },
|
|
22
|
-
{ name: 'metadata', type: { T: 'jsonb', null: true, category: 'json' } },
|
|
23
|
-
{ name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
|
|
24
|
-
{ name: 'tags', type: { T: 'text[]', null: true, category: 'array' } },
|
|
25
|
-
{ name: 'avatar', type: { T: 'bytea', null: true, category: 'binary' } },
|
|
26
|
-
{ name: 'balance', type: { T: 'numeric(10,2)', null: true, category: 'decimal' } },
|
|
27
|
-
{ name: 'external_id', type: { T: 'uuid', null: true, category: 'uuid' } },
|
|
28
|
-
],
|
|
29
|
-
primary_key: { parts: [{ column: 'id' }] },
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'posts',
|
|
33
|
-
columns: [
|
|
34
|
-
{ name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
|
|
35
|
-
{ name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
|
|
36
|
-
{ name: 'title', type: { T: 'text', null: false, category: 'string' } },
|
|
37
|
-
{ name: 'body', type: { T: 'text', null: false, category: 'string' } },
|
|
38
|
-
{ name: 'published_at', type: { T: 'timestamp with time zone', null: true, category: 'time' } },
|
|
39
|
-
{ name: 'view_count', type: { T: 'integer', null: false, category: 'integer' } },
|
|
40
|
-
{ name: 'rating', type: { T: 'double precision', null: true, category: 'float' } },
|
|
41
|
-
],
|
|
42
|
-
primary_key: { parts: [{ column: 'id' }] },
|
|
43
|
-
foreign_keys: [
|
|
44
|
-
{ symbol: 'posts_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'comments',
|
|
49
|
-
columns: [
|
|
50
|
-
{ name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
|
|
51
|
-
{ name: 'post_id', type: { T: 'bigint', null: false, category: 'integer' } },
|
|
52
|
-
{ name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
|
|
53
|
-
{ name: 'content', type: { T: 'text', null: false, category: 'string' } },
|
|
54
|
-
{ name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
|
|
55
|
-
],
|
|
56
|
-
primary_key: { parts: [{ column: 'id' }] },
|
|
57
|
-
foreign_keys: [
|
|
58
|
-
{ symbol: 'comments_post_id_fkey', columns: ['post_id'], ref_table: 'posts', ref_columns: ['id'] },
|
|
59
|
-
{ symbol: 'comments_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
|
|
60
|
-
],
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
|
|
68
|
-
return {
|
|
69
|
-
realm: testRealm,
|
|
70
|
-
allFileTags: [],
|
|
71
|
-
docsMeta: [],
|
|
72
|
-
config: {},
|
|
73
|
-
output: './out',
|
|
74
|
-
templateName: 'go-structs',
|
|
75
|
-
...overrides,
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
describe('go-structs template', () => {
|
|
80
|
-
it('generates Go structs with json tags', () => {
|
|
81
|
-
const result = generate(makeCtx())
|
|
82
|
-
expect(result.files).toHaveLength(1)
|
|
83
|
-
expect(result.files[0].path).toBe('models.go')
|
|
84
|
-
|
|
85
|
-
const content = result.files[0].content
|
|
86
|
-
expect(content).toContain('type Users struct {')
|
|
87
|
-
expect(content).toContain('type Posts struct {')
|
|
88
|
-
expect(content).toContain('type Comments struct {')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('maps bigserial to int64', () => {
|
|
92
|
-
const result = generate(makeCtx())
|
|
93
|
-
const content = result.files[0].content
|
|
94
|
-
expect(content).toContain('Id int64')
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('maps nullable text to *string', () => {
|
|
98
|
-
const result = generate(makeCtx())
|
|
99
|
-
const content = result.files[0].content
|
|
100
|
-
expect(content).toContain('Name *string')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('maps text[] to []string', () => {
|
|
104
|
-
const result = generate(makeCtx())
|
|
105
|
-
const content = result.files[0].content
|
|
106
|
-
expect(content).toContain('Tags []string')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('includes time import for timestamptz', () => {
|
|
110
|
-
const result = generate(makeCtx())
|
|
111
|
-
const content = result.files[0].content
|
|
112
|
-
expect(content).toContain('import (')
|
|
113
|
-
expect(content).toContain('"time"')
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('uses omitempty for nullable json tags', () => {
|
|
117
|
-
const result = generate(makeCtx())
|
|
118
|
-
const content = result.files[0].content
|
|
119
|
-
expect(content).toContain('`json:"name,omitempty"`')
|
|
120
|
-
expect(content).toContain('`json:"email"`')
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('respects @codegen.skip tag', () => {
|
|
124
|
-
const ctx = makeCtx({
|
|
125
|
-
allFileTags: [
|
|
126
|
-
{
|
|
127
|
-
sourceFile: 'test.sql',
|
|
128
|
-
objects: [
|
|
129
|
-
{
|
|
130
|
-
objectName: 'comments',
|
|
131
|
-
target: 'table',
|
|
132
|
-
tags: [{ namespace: 'codegen', tag: 'skip', args: [] }],
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
})
|
|
138
|
-
const result = generate(ctx)
|
|
139
|
-
const content = result.files[0].content
|
|
140
|
-
expect(content).not.toContain('type Comments struct')
|
|
141
|
-
expect(content).toContain('type Users struct')
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('respects @codegen.rename tag', () => {
|
|
145
|
-
const ctx = makeCtx({
|
|
146
|
-
allFileTags: [
|
|
147
|
-
{
|
|
148
|
-
sourceFile: 'test.sql',
|
|
149
|
-
objects: [
|
|
150
|
-
{
|
|
151
|
-
objectName: 'users',
|
|
152
|
-
target: 'table',
|
|
153
|
-
tags: [{ namespace: 'codegen', tag: 'rename', args: ['Account'] }],
|
|
154
|
-
},
|
|
155
|
-
],
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
})
|
|
159
|
-
const result = generate(ctx)
|
|
160
|
-
const content = result.files[0].content
|
|
161
|
-
expect(content).toContain('type Account struct')
|
|
162
|
-
expect(content).not.toContain('type Users struct')
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('respects @codegen.type override on column', () => {
|
|
166
|
-
const ctx = makeCtx({
|
|
167
|
-
allFileTags: [
|
|
168
|
-
{
|
|
169
|
-
sourceFile: 'test.sql',
|
|
170
|
-
objects: [
|
|
171
|
-
{
|
|
172
|
-
objectName: 'users.metadata',
|
|
173
|
-
target: 'column',
|
|
174
|
-
tags: [{ namespace: 'codegen', tag: 'type', args: ['map[string]interface{}'] }],
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
})
|
|
180
|
-
const result = generate(ctx)
|
|
181
|
-
const content = result.files[0].content
|
|
182
|
-
expect(content).toContain('Metadata map[string]interface{}')
|
|
183
|
-
})
|
|
184
|
-
})
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { toCamelCase, toPascalCase, toScreamingSnake } from '../helpers/naming'
|
|
3
|
-
|
|
4
|
-
describe('toPascalCase', () => {
|
|
5
|
-
it('converts snake_case to PascalCase', () => {
|
|
6
|
-
expect(toPascalCase('user_accounts')).toBe('UserAccounts')
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
it('handles single word', () => {
|
|
10
|
-
expect(toPascalCase('id')).toBe('Id')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('handles leading underscore', () => {
|
|
14
|
-
expect(toPascalCase('_private_field')).toBe('PrivateField')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('handles multiple underscores', () => {
|
|
18
|
-
expect(toPascalCase('user__name')).toBe('UserName')
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('handles already capitalized segments', () => {
|
|
22
|
-
expect(toPascalCase('USER_STATUS')).toBe('UserStatus')
|
|
23
|
-
})
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
describe('toCamelCase', () => {
|
|
27
|
-
it('converts snake_case to camelCase', () => {
|
|
28
|
-
expect(toCamelCase('user_accounts')).toBe('userAccounts')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('handles single word', () => {
|
|
32
|
-
expect(toCamelCase('name')).toBe('name')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('produces lowercase first char', () => {
|
|
36
|
-
expect(toCamelCase('ID')).toBe('id')
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
describe('toScreamingSnake', () => {
|
|
41
|
-
it('converts to SCREAMING_SNAKE', () => {
|
|
42
|
-
expect(toScreamingSnake('user_status')).toBe('USER_STATUS')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('handles already lowercase', () => {
|
|
46
|
-
expect(toScreamingSnake('created_at')).toBe('CREATED_AT')
|
|
47
|
-
})
|
|
48
|
-
})
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import pythonDataclasses from '../python-dataclasses/index.ts'
|
|
3
|
-
|
|
4
|
-
const generate = pythonDataclasses.generate
|
|
5
|
-
|
|
6
|
-
import type { AtlasRealm } from '@sqldoc/atlas'
|
|
7
|
-
import type { TemplateContext } from '@sqldoc/ns-codegen'
|
|
8
|
-
|
|
9
|
-
const testRealm: AtlasRealm = {
|
|
10
|
-
schemas: [
|
|
11
|
-
{
|
|
12
|
-
name: 'public',
|
|
13
|
-
tables: [
|
|
14
|
-
{
|
|
15
|
-
name: 'users',
|
|
16
|
-
columns: [
|
|
17
|
-
{ name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
|
|
18
|
-
{ name: 'email', type: { T: 'character varying', raw: 'varchar(255)', null: false, category: 'string' } },
|
|
19
|
-
{ name: 'name', type: { T: 'text', null: true, category: 'string' } },
|
|
20
|
-
{ name: 'age', type: { T: 'integer', null: true, category: 'integer' } },
|
|
21
|
-
{ name: 'is_active', type: { T: 'boolean', null: false, category: 'boolean' } },
|
|
22
|
-
{ name: 'metadata', type: { T: 'jsonb', null: true, category: 'json' } },
|
|
23
|
-
{ name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
|
|
24
|
-
{ name: 'tags', type: { T: 'text[]', null: true, category: 'array' } },
|
|
25
|
-
{ name: 'avatar', type: { T: 'bytea', null: true, category: 'binary' } },
|
|
26
|
-
{ name: 'balance', type: { T: 'numeric(10,2)', null: true, category: 'decimal' } },
|
|
27
|
-
{ name: 'external_id', type: { T: 'uuid', null: true, category: 'uuid' } },
|
|
28
|
-
],
|
|
29
|
-
primary_key: { parts: [{ column: 'id' }] },
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'posts',
|
|
33
|
-
columns: [
|
|
34
|
-
{ name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
|
|
35
|
-
{ name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
|
|
36
|
-
{ name: 'title', type: { T: 'text', null: false, category: 'string' } },
|
|
37
|
-
{ name: 'body', type: { T: 'text', null: false, category: 'string' } },
|
|
38
|
-
{ name: 'published_at', type: { T: 'timestamp with time zone', null: true, category: 'time' } },
|
|
39
|
-
{ name: 'view_count', type: { T: 'integer', null: false, category: 'integer' } },
|
|
40
|
-
{ name: 'rating', type: { T: 'double precision', null: true, category: 'float' } },
|
|
41
|
-
],
|
|
42
|
-
primary_key: { parts: [{ column: 'id' }] },
|
|
43
|
-
foreign_keys: [
|
|
44
|
-
{ symbol: 'posts_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'comments',
|
|
49
|
-
columns: [
|
|
50
|
-
{ name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
|
|
51
|
-
{ name: 'post_id', type: { T: 'bigint', null: false, category: 'integer' } },
|
|
52
|
-
{ name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
|
|
53
|
-
{ name: 'content', type: { T: 'text', null: false, category: 'string' } },
|
|
54
|
-
{ name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
|
|
55
|
-
],
|
|
56
|
-
primary_key: { parts: [{ column: 'id' }] },
|
|
57
|
-
foreign_keys: [
|
|
58
|
-
{ symbol: 'comments_post_id_fkey', columns: ['post_id'], ref_table: 'posts', ref_columns: ['id'] },
|
|
59
|
-
{ symbol: 'comments_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
|
|
60
|
-
],
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
|
|
68
|
-
return {
|
|
69
|
-
realm: testRealm,
|
|
70
|
-
allFileTags: [],
|
|
71
|
-
docsMeta: [],
|
|
72
|
-
config: {},
|
|
73
|
-
output: './out',
|
|
74
|
-
templateName: 'python-dataclasses',
|
|
75
|
-
...overrides,
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
describe('python-dataclasses template', () => {
|
|
80
|
-
it('generates dataclass definitions', () => {
|
|
81
|
-
const result = generate(makeCtx())
|
|
82
|
-
expect(result.files).toHaveLength(1)
|
|
83
|
-
expect(result.files[0].path).toBe('models.py')
|
|
84
|
-
|
|
85
|
-
const content = result.files[0].content
|
|
86
|
-
expect(content).toContain('@dataclass')
|
|
87
|
-
expect(content).toContain('class Users:')
|
|
88
|
-
expect(content).toContain('class Posts:')
|
|
89
|
-
expect(content).toContain('class Comments:')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('maps bigserial to int', () => {
|
|
93
|
-
const result = generate(makeCtx())
|
|
94
|
-
const content = result.files[0].content
|
|
95
|
-
expect(content).toContain('id: int')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('maps nullable text to Optional[str]', () => {
|
|
99
|
-
const result = generate(makeCtx())
|
|
100
|
-
const content = result.files[0].content
|
|
101
|
-
expect(content).toContain('name: Optional[str]')
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('maps nullable text[] to Optional[list[str]]', () => {
|
|
105
|
-
const result = generate(makeCtx())
|
|
106
|
-
const content = result.files[0].content
|
|
107
|
-
expect(content).toContain('tags: Optional[list[str]]')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('includes proper Python imports', () => {
|
|
111
|
-
const result = generate(makeCtx())
|
|
112
|
-
const content = result.files[0].content
|
|
113
|
-
expect(content).toContain('from dataclasses import dataclass')
|
|
114
|
-
expect(content).toContain('from typing import Optional')
|
|
115
|
-
expect(content).toContain('from datetime import datetime')
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('nullable fields have = None default', () => {
|
|
119
|
-
const result = generate(makeCtx())
|
|
120
|
-
const content = result.files[0].content
|
|
121
|
-
expect(content).toContain('name: Optional[str] = None')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('respects @codegen.skip tag', () => {
|
|
125
|
-
const ctx = makeCtx({
|
|
126
|
-
allFileTags: [
|
|
127
|
-
{
|
|
128
|
-
sourceFile: 'test.sql',
|
|
129
|
-
objects: [
|
|
130
|
-
{
|
|
131
|
-
objectName: 'comments',
|
|
132
|
-
target: 'table',
|
|
133
|
-
tags: [{ namespace: 'codegen', tag: 'skip', args: [] }],
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
})
|
|
139
|
-
const result = generate(ctx)
|
|
140
|
-
const content = result.files[0].content
|
|
141
|
-
expect(content).not.toContain('class Comments:')
|
|
142
|
-
expect(content).toContain('class Users:')
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('respects @codegen.rename tag', () => {
|
|
146
|
-
const ctx = makeCtx({
|
|
147
|
-
allFileTags: [
|
|
148
|
-
{
|
|
149
|
-
sourceFile: 'test.sql',
|
|
150
|
-
objects: [
|
|
151
|
-
{
|
|
152
|
-
objectName: 'users',
|
|
153
|
-
target: 'table',
|
|
154
|
-
tags: [{ namespace: 'codegen', tag: 'rename', args: ['Account'] }],
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
})
|
|
160
|
-
const result = generate(ctx)
|
|
161
|
-
const content = result.files[0].content
|
|
162
|
-
expect(content).toContain('class Account:')
|
|
163
|
-
expect(content).not.toContain('class Users:')
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('respects @codegen.type override on column', () => {
|
|
167
|
-
const ctx = makeCtx({
|
|
168
|
-
allFileTags: [
|
|
169
|
-
{
|
|
170
|
-
sourceFile: 'test.sql',
|
|
171
|
-
objects: [
|
|
172
|
-
{
|
|
173
|
-
objectName: 'users.metadata',
|
|
174
|
-
target: 'column',
|
|
175
|
-
tags: [{ namespace: 'codegen', tag: 'type', args: ['dict[str, Any]'] }],
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
},
|
|
179
|
-
],
|
|
180
|
-
})
|
|
181
|
-
const result = generate(ctx)
|
|
182
|
-
const content = result.files[0].content
|
|
183
|
-
expect(content).toContain('metadata: dict[str, Any]')
|
|
184
|
-
})
|
|
185
|
-
})
|