sqlcx-orm 0.1.0

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.
@@ -0,0 +1,143 @@
1
+ import type { SchemaGenerator } from "@/generator/interface";
2
+ import type { SqlcxIR, TableDef, EnumDef, SqlType, ColumnDef, JsonShape } from "@/ir";
3
+ import { pascalCase } from "@/utils";
4
+
5
+ /** Safely escape a string for embedding in generated JS/TS double-quoted literals */
6
+ function escapeString(str: string): string {
7
+ return JSON.stringify(str).slice(1, -1); // strip outer quotes from JSON.stringify
8
+ }
9
+
10
+ function jsonShapeToTypeBox(shape: JsonShape): string {
11
+ switch (shape.kind) {
12
+ case "string":
13
+ return "Type.String()";
14
+ case "number":
15
+ return "Type.Number()";
16
+ case "boolean":
17
+ return "Type.Boolean()";
18
+ case "object": {
19
+ const fields = Object.entries(shape.fields)
20
+ .map(([key, val]) => `"${escapeString(key)}": ${jsonShapeToTypeBox(val)}`)
21
+ .join(", ");
22
+ return `Type.Object({ ${fields} })`;
23
+ }
24
+ case "array":
25
+ return `Type.Array(${jsonShapeToTypeBox(shape.element)})`;
26
+ case "nullable":
27
+ return `Type.Union([${jsonShapeToTypeBox(shape.inner)}, Type.Null()])`;
28
+ }
29
+ }
30
+
31
+ function typeBoxType(type: SqlType): string {
32
+ // Inline @enum annotation takes precedence
33
+ if (type.enumValues) {
34
+ const literals = type.enumValues
35
+ .map((v) => `Type.Literal("${escapeString(v)}")`)
36
+ .join(", ");
37
+ return `Type.Union([${literals}])`;
38
+ }
39
+
40
+ // Inline @json annotation takes precedence over generic json category
41
+ if (type.jsonShape) {
42
+ return jsonShapeToTypeBox(type.jsonShape);
43
+ }
44
+
45
+ if (type.elementType) {
46
+ return `Type.Array(${typeBoxType(type.elementType)})`;
47
+ }
48
+
49
+ switch (type.category) {
50
+ case "string":
51
+ return "Type.String()";
52
+ case "number":
53
+ return "Type.Number()";
54
+ case "boolean":
55
+ return "Type.Boolean()";
56
+ case "date":
57
+ return "Type.Date()";
58
+ case "json":
59
+ return "Type.Any()";
60
+ case "uuid":
61
+ return "Type.String()";
62
+ case "binary":
63
+ return "Type.Uint8Array()";
64
+ case "enum": {
65
+ if (type.enumName) {
66
+ return pascalCase(type.enumName);
67
+ }
68
+ return "Type.String()";
69
+ }
70
+ case "unknown":
71
+ return "Type.Unknown()";
72
+ default: {
73
+ const _exhaustive: never = type.category;
74
+ return _exhaustive;
75
+ }
76
+ }
77
+ }
78
+
79
+ function selectColumn(col: ColumnDef): string {
80
+ const base = typeBoxType(col.type);
81
+ if (col.nullable) {
82
+ return `Type.Union([${base}, Type.Null()])`;
83
+ }
84
+ return base;
85
+ }
86
+
87
+ function insertColumn(col: ColumnDef): string {
88
+ const base = typeBoxType(col.type);
89
+ if (col.hasDefault) {
90
+ if (col.nullable) {
91
+ return `Type.Optional(Type.Union([${base}, Type.Null()]))`;
92
+ }
93
+ return `Type.Optional(${base})`;
94
+ }
95
+ if (col.nullable) {
96
+ return `Type.Optional(Type.Union([${base}, Type.Null()]))`;
97
+ }
98
+ return base;
99
+ }
100
+
101
+ function objectBody(
102
+ columns: ColumnDef[],
103
+ mapper: (col: ColumnDef) => string,
104
+ ): string {
105
+ const fields = columns
106
+ .map((col) => ` "${escapeString(col.name)}": ${mapper(col)}`)
107
+ .join(",\n");
108
+ return `{\n${fields}\n}`;
109
+ }
110
+
111
+ export function createTypeBoxGenerator(): SchemaGenerator {
112
+ return {
113
+ name: "typebox",
114
+
115
+ generateImports(): string {
116
+ return `import { Type, type Static } from "@sinclair/typebox";\n\n// Requires @sinclair/typebox >= 0.31.0 (for Type.Date and Type.Uint8Array)\n\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};`;
117
+ },
118
+
119
+ generateEnumSchema(enumDef: EnumDef): string {
120
+ const name = pascalCase(enumDef.name);
121
+ const literals = enumDef.values
122
+ .map((v) => `Type.Literal("${escapeString(v)}")`)
123
+ .join(", ");
124
+ return `export const ${name} = Type.Union([${literals}]);`;
125
+ },
126
+
127
+ generateSelectSchema(table: TableDef, _ir: SqlcxIR): string {
128
+ const name = `Select${pascalCase(table.name)}`;
129
+ const body = objectBody(table.columns, selectColumn);
130
+ return `export const ${name} = Type.Object(${body});`;
131
+ },
132
+
133
+ generateInsertSchema(table: TableDef, _ir: SqlcxIR): string {
134
+ const name = `Insert${pascalCase(table.name)}`;
135
+ const body = objectBody(table.columns, insertColumn);
136
+ return `export const ${name} = Type.Object(${body});`;
137
+ },
138
+
139
+ generateTypeAlias(name: string, schemaVarName: string): string {
140
+ return `export type ${name} = Prettify<Static<typeof ${schemaVarName}>>;`;
141
+ },
142
+ };
143
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ // Public API
2
+ export { defineConfig } from "@/config";
3
+ export type { SqlcxConfig } from "@/config";
4
+ export type {
5
+ SqlcxIR,
6
+ TableDef,
7
+ ColumnDef,
8
+ QueryDef,
9
+ ParamDef,
10
+ EnumDef,
11
+ SqlType,
12
+ SqlTypeCategory,
13
+ QueryCommand,
14
+ JsonShape,
15
+ } from "@/ir";
16
+ export type { DatabaseParser } from "@/parser/interface";
17
+ export type {
18
+ LanguagePlugin,
19
+ SchemaGenerator,
20
+ DriverGenerator,
21
+ GeneratedFile,
22
+ LanguageOptions,
23
+ } from "@/generator/interface";
@@ -0,0 +1,72 @@
1
+ export type SqlTypeCategory =
2
+ | "string"
3
+ | "number"
4
+ | "boolean"
5
+ | "date"
6
+ | "json"
7
+ | "uuid"
8
+ | "binary"
9
+ | "enum"
10
+ | "unknown";
11
+
12
+ export type JsonShape =
13
+ | { kind: "string" }
14
+ | { kind: "number" }
15
+ | { kind: "boolean" }
16
+ | { kind: "object"; fields: Record<string, JsonShape> }
17
+ | { kind: "array"; element: JsonShape }
18
+ | { kind: "nullable"; inner: JsonShape };
19
+
20
+ export interface SqlType {
21
+ raw: string;
22
+ normalized: string;
23
+ category: SqlTypeCategory;
24
+ elementType?: SqlType;
25
+ enumName?: string;
26
+ enumValues?: string[];
27
+ jsonShape?: JsonShape;
28
+ }
29
+
30
+ export interface ColumnDef {
31
+ name: string;
32
+ alias?: string;
33
+ sourceTable?: string;
34
+ type: SqlType;
35
+ nullable: boolean;
36
+ hasDefault: boolean;
37
+ }
38
+
39
+ export interface TableDef {
40
+ name: string;
41
+ columns: ColumnDef[];
42
+ primaryKey: string[];
43
+ uniqueConstraints: string[][];
44
+ }
45
+
46
+ export type QueryCommand = "one" | "many" | "exec" | "execresult";
47
+
48
+ export interface ParamDef {
49
+ index: number;
50
+ name: string;
51
+ type: SqlType;
52
+ }
53
+
54
+ export interface QueryDef {
55
+ name: string;
56
+ command: QueryCommand;
57
+ sql: string;
58
+ params: ParamDef[];
59
+ returns: ColumnDef[];
60
+ sourceFile: string;
61
+ }
62
+
63
+ export interface EnumDef {
64
+ name: string;
65
+ values: string[];
66
+ }
67
+
68
+ export interface SqlcxIR {
69
+ tables: TableDef[];
70
+ queries: QueryDef[];
71
+ enums: EnumDef[];
72
+ }
@@ -0,0 +1,8 @@
1
+ import type { TableDef, QueryDef, EnumDef } from "@/ir";
2
+
3
+ export interface DatabaseParser {
4
+ dialect: string;
5
+ parseSchema(sql: string): TableDef[];
6
+ parseQueries(sql: string, tables: TableDef[]): QueryDef[];
7
+ parseEnums(sql: string): EnumDef[];
8
+ }
@@ -0,0 +1,49 @@
1
+ export interface RawParam {
2
+ index: number;
3
+ column: string | null;
4
+ override?: string;
5
+ }
6
+
7
+ export function resolveParamNames(params: RawParam[]): string[] {
8
+ // Pass 1: count column frequency (needed to know which columns collide)
9
+ const freq = new Map<string, number>();
10
+ for (const p of params) {
11
+ if (!p.override && p.column) {
12
+ freq.set(p.column, (freq.get(p.column) ?? 0) + 1);
13
+ }
14
+ }
15
+
16
+ // Pass 2: assign names + dedup in one go
17
+ const counters = new Map<string, number>();
18
+ const seen = new Set<string>();
19
+ const result: string[] = new Array(params.length);
20
+
21
+ for (let i = 0; i < params.length; i++) {
22
+ const p = params[i];
23
+ let name: string;
24
+
25
+ if (p.override) {
26
+ name = p.override;
27
+ } else if (!p.column) {
28
+ name = `param_${p.index}`;
29
+ } else if ((freq.get(p.column) ?? 0) > 1) {
30
+ const n = (counters.get(p.column) ?? 0) + 1;
31
+ counters.set(p.column, n);
32
+ name = `${p.column}_${n}`;
33
+ } else {
34
+ name = p.column;
35
+ }
36
+
37
+ // Dedup: resolve any remaining collisions (override-vs-inferred, suffix-vs-literal)
38
+ const base = name;
39
+ let suffix = 1;
40
+ while (seen.has(name)) {
41
+ name = `${base}_${suffix++}`;
42
+ }
43
+
44
+ seen.add(name);
45
+ result[i] = name;
46
+ }
47
+
48
+ return result;
49
+ }