@typesql/pg 0.1.5 → 0.2.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.
- package/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -3
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/introspect/cli.d.ts.map +1 -1
- package/dist/introspect/cli.js +26 -18
- package/dist/introspect/cli.js.map +1 -1
- package/dist/introspect/emit.d.ts.map +1 -1
- package/dist/introspect/emit.js +48 -11
- package/dist/introspect/emit.js.map +1 -1
- package/dist/introspect/index.d.ts +3 -4
- package/dist/introspect/index.d.ts.map +1 -1
- package/dist/introspect/index.js +1 -3
- package/dist/introspect/index.js.map +1 -1
- package/dist/introspect/introspect.d.ts.map +1 -1
- package/dist/introspect/introspect.js +40 -8
- package/dist/introspect/introspect.js.map +1 -1
- package/dist/introspect/nullability.d.ts +2 -1
- package/dist/introspect/nullability.d.ts.map +1 -1
- package/dist/introspect/nullability.js +101 -29
- package/dist/introspect/nullability.js.map +1 -1
- package/dist/type-map.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +63 -39
- package/src/index.ts +32 -15
- package/src/introspect/cli.ts +124 -100
- package/src/introspect/emit.ts +224 -139
- package/src/introspect/index.ts +9 -10
- package/src/introspect/introspect.ts +239 -155
- package/src/introspect/nullability.ts +399 -257
- package/src/type-map.ts +25 -24
package/src/introspect/cli.ts
CHANGED
|
@@ -3,15 +3,15 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import type { PgQueryable } from "../client.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { type GenerateOptions, generateSource } from "./emit.js";
|
|
7
|
+
import { type IntrospectOptions, introspect } from "./introspect.js";
|
|
8
8
|
|
|
9
9
|
/** What a `--config` module exports as its default: CLI settings plus every generator option. */
|
|
10
10
|
export interface IntrospectConfig extends GenerateOptions, IntrospectOptions {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/** Connection string. Default: `--url`, then `$DATABASE_URL`. */
|
|
12
|
+
readonly url?: string;
|
|
13
|
+
/** Output file, relative to the working directory. Default: stdout. */
|
|
14
|
+
readonly out?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const USAGE = `Usage: typesql introspect [options]
|
|
@@ -32,107 +32,131 @@ Options:
|
|
|
32
32
|
`;
|
|
33
33
|
|
|
34
34
|
interface Io {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
readonly stdout: (s: string) => void;
|
|
36
|
+
readonly stderr: (s: string) => void;
|
|
37
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
38
|
+
readonly cwd: string;
|
|
39
|
+
/** Opens a client for a connection string; default: a `pg.Client`. */
|
|
40
|
+
readonly connect?: (
|
|
41
|
+
url: string,
|
|
42
|
+
) => Promise<PgQueryable & { end(): Promise<void> }>;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
const defaultIo = (): Io => ({
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
stdout: (s) => process.stdout.write(s),
|
|
47
|
+
stderr: (s) => process.stderr.write(s),
|
|
48
|
+
env: process.env,
|
|
49
|
+
cwd: process.cwd(),
|
|
48
50
|
});
|
|
49
51
|
|
|
50
|
-
async function pgConnect(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
async function pgConnect(
|
|
53
|
+
url: string,
|
|
54
|
+
): Promise<PgQueryable & { end(): Promise<void> }> {
|
|
55
|
+
let Client: typeof import("pg").Client;
|
|
56
|
+
try {
|
|
57
|
+
({ Client } = (await import("pg")).default);
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'typesql introspect needs the "pg" package: npm install pg',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const client = new Client({ connectionString: url });
|
|
64
|
+
await client.connect();
|
|
65
|
+
return client;
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
const parse = (argv: readonly string[]) =>
|
|
69
|
+
parseArgs({
|
|
70
|
+
args: [...argv],
|
|
71
|
+
allowPositionals: true,
|
|
72
|
+
options: {
|
|
73
|
+
url: { type: "string" },
|
|
74
|
+
out: { type: "string" },
|
|
75
|
+
schema: { type: "string", multiple: true },
|
|
76
|
+
exclude: { type: "string", multiple: true },
|
|
77
|
+
"type-name": { type: "string" },
|
|
78
|
+
"import-from": { type: "string" },
|
|
79
|
+
config: { type: "string" },
|
|
80
|
+
"no-comments": { type: "boolean" },
|
|
81
|
+
help: { type: "boolean", short: "h" },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
62
85
|
/** Runs the `typesql` CLI with `argv` (without `node` and the script). Returns the exit code. */
|
|
63
|
-
export async function main(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return 2;
|
|
84
|
-
}
|
|
85
|
-
const { values, positionals } = parsed;
|
|
86
|
-
if (values.help || positionals.length === 0) {
|
|
87
|
-
(values.help ? io.stdout : io.stderr)(USAGE);
|
|
88
|
-
return values.help ? 0 : 2;
|
|
89
|
-
}
|
|
90
|
-
if (positionals[0] !== "introspect" || positionals.length > 1) {
|
|
91
|
-
io.stderr(`unknown command: ${positionals.join(" ")}\n\n${USAGE}`);
|
|
92
|
-
return 2;
|
|
93
|
-
}
|
|
86
|
+
export async function main(
|
|
87
|
+
argv: readonly string[],
|
|
88
|
+
io: Io = defaultIo(),
|
|
89
|
+
): Promise<number> {
|
|
90
|
+
let parsed: ReturnType<typeof parse>;
|
|
91
|
+
try {
|
|
92
|
+
parsed = parse(argv);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
io.stderr(`${(err as Error).message}\n\n${USAGE}`);
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
const { values, positionals } = parsed;
|
|
98
|
+
if (values.help || positionals.length === 0) {
|
|
99
|
+
(values.help ? io.stdout : io.stderr)(USAGE);
|
|
100
|
+
return values.help ? 0 : 2;
|
|
101
|
+
}
|
|
102
|
+
if (positionals[0] !== "introspect" || positionals.length > 1) {
|
|
103
|
+
io.stderr(`unknown command: ${positionals.join(" ")}\n\n${USAGE}`);
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
107
|
+
try {
|
|
108
|
+
let config: IntrospectConfig = {};
|
|
109
|
+
if (values.config) {
|
|
110
|
+
const mod = (await import(
|
|
111
|
+
pathToFileURL(resolve(io.cwd, values.config)).href
|
|
112
|
+
)) as { default?: IntrospectConfig };
|
|
113
|
+
config = mod.default ?? {};
|
|
114
|
+
}
|
|
115
|
+
const url = values.url ?? config.url ?? io.env["DATABASE_URL"];
|
|
116
|
+
if (!url) {
|
|
117
|
+
io.stderr("typesql introspect: pass --url or set DATABASE_URL\n");
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
const list = (v: string[] | undefined) =>
|
|
121
|
+
v?.flatMap((s) =>
|
|
122
|
+
s
|
|
123
|
+
.split(",")
|
|
124
|
+
.map((x) => x.trim())
|
|
125
|
+
.filter(Boolean),
|
|
126
|
+
);
|
|
127
|
+
const schemas = list(values.schema) ?? config.schemas;
|
|
128
|
+
const exclude = list(values.exclude) ?? config.exclude;
|
|
129
|
+
const options: IntrospectConfig = {
|
|
130
|
+
...config,
|
|
131
|
+
...(schemas !== undefined && { schemas }),
|
|
132
|
+
...(exclude !== undefined && { exclude }),
|
|
133
|
+
...(values["type-name"] !== undefined && {
|
|
134
|
+
typeName: values["type-name"],
|
|
135
|
+
}),
|
|
136
|
+
...(values["import-from"] !== undefined && {
|
|
137
|
+
importFrom: values["import-from"],
|
|
138
|
+
}),
|
|
139
|
+
...(values["no-comments"] && { comments: false }),
|
|
140
|
+
};
|
|
141
|
+
const client = await (io.connect ?? pgConnect)(url);
|
|
142
|
+
let source: string;
|
|
143
|
+
try {
|
|
144
|
+
source = generateSource(await introspect(client, options), options);
|
|
145
|
+
} finally {
|
|
146
|
+
await client.end();
|
|
147
|
+
}
|
|
148
|
+
const out = values.out ?? config.out;
|
|
149
|
+
if (out === undefined) {
|
|
150
|
+
io.stdout(source);
|
|
151
|
+
} else {
|
|
152
|
+
const file = resolve(io.cwd, out);
|
|
153
|
+
await mkdir(dirname(file), { recursive: true });
|
|
154
|
+
await writeFile(file, source);
|
|
155
|
+
io.stderr(`typesql introspect: wrote ${out}\n`);
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
io.stderr(`typesql introspect: ${(err as Error).message}\n`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
138
162
|
}
|
package/src/introspect/emit.ts
CHANGED
|
@@ -1,53 +1,79 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
IntrospectedColumn,
|
|
3
|
+
IntrospectedDatabase,
|
|
4
|
+
IntrospectedRelation,
|
|
5
|
+
} from "./introspect.js";
|
|
2
6
|
|
|
3
7
|
export interface GenerateOptions {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
/** Name of the exported schema interface. Default `"DB"`. */
|
|
9
|
+
readonly typeName?: string;
|
|
10
|
+
/** Module the column helpers are imported from. Default `"@typesql/core"`. */
|
|
11
|
+
readonly importFrom?: string;
|
|
12
|
+
/** Extra lines after the helper import, e.g. `import type { HistoryItem } from "./json.js";`. */
|
|
13
|
+
readonly imports?: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* TypeScript types of `json` / `jsonb` columns: a map keyed `"table.column"` (public) or
|
|
16
|
+
* `"schema.table.column"`, or a function returning the TS type source for a column (or
|
|
17
|
+
* `undefined`). Such a column is declared as `Json<T>`: still jsonb to the SQL, read as `T`,
|
|
18
|
+
* written with `sql.json(value)`. Other columns are an error (use `column` for those).
|
|
19
|
+
*/
|
|
20
|
+
readonly columnTypes?:
|
|
21
|
+
| Readonly<Record<string, string>>
|
|
22
|
+
| ((
|
|
23
|
+
column: IntrospectedColumn,
|
|
24
|
+
relation: IntrospectedRelation,
|
|
25
|
+
) => string | undefined);
|
|
26
|
+
/**
|
|
27
|
+
* Full control over one column: return the TypeScript source of its descriptor
|
|
28
|
+
* (`Col<"jsonb">`, a helper of your own, ...) or `undefined` to keep the default.
|
|
29
|
+
*/
|
|
30
|
+
readonly column?: (
|
|
31
|
+
column: IntrospectedColumn,
|
|
32
|
+
relation: IntrospectedRelation,
|
|
33
|
+
) => string | undefined;
|
|
34
|
+
/** Emit table and column comments and notes as JSDoc. Default `true`. */
|
|
35
|
+
readonly comments?: boolean;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
29
|
-
const key = (name: string): string =>
|
|
39
|
+
const key = (name: string): string =>
|
|
40
|
+
IDENT.test(name) ? name : JSON.stringify(name);
|
|
30
41
|
const str = (s: string): string => JSON.stringify(s);
|
|
31
42
|
|
|
32
43
|
function doc(lines: readonly string[], indent: string): string {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
const text = lines
|
|
45
|
+
.flatMap((l) => l.split("\n"))
|
|
46
|
+
.map((l) => l.replace(/\*\//g, "*\\/").trimEnd());
|
|
47
|
+
if (text.length === 0) return "";
|
|
48
|
+
if (text.length === 1) return `${indent}/** ${text[0]} */\n`;
|
|
49
|
+
return `${indent}/**\n${text.map((l) => `${indent} *${l ? ` ${l}` : ""}`).join("\n")}\n${indent} */\n`;
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
type Helper = "Col" | "Nullable" | "Generated" | "NullableGenerated" | "Json";
|
|
40
53
|
const helperFor = (nullable: boolean, hasDefault: boolean): Helper =>
|
|
41
|
-
|
|
54
|
+
nullable
|
|
55
|
+
? hasDefault
|
|
56
|
+
? "NullableGenerated"
|
|
57
|
+
: "Nullable"
|
|
58
|
+
: hasDefault
|
|
59
|
+
? "Generated"
|
|
60
|
+
: "Col";
|
|
42
61
|
|
|
43
62
|
function columnNotes(c: IntrospectedColumn): string[] {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
const notes: string[] = [];
|
|
64
|
+
if (c.comment) notes.push(c.comment);
|
|
65
|
+
if (c.domain) notes.push(`Domain \`${c.domain}\`.`);
|
|
66
|
+
else if (c.declaredType === "citext")
|
|
67
|
+
notes.push("`citext`: typed as `text`.");
|
|
68
|
+
if (c.generated !== null)
|
|
69
|
+
notes.push(
|
|
70
|
+
`\`GENERATED ALWAYS AS (${c.generated}) STORED\`: cannot be written.`,
|
|
71
|
+
);
|
|
72
|
+
if (c.identity === "always")
|
|
73
|
+
notes.push(
|
|
74
|
+
"`GENERATED ALWAYS AS IDENTITY`: written only with `OVERRIDING SYSTEM VALUE`.",
|
|
75
|
+
);
|
|
76
|
+
return notes;
|
|
51
77
|
}
|
|
52
78
|
|
|
53
79
|
/**
|
|
@@ -55,114 +81,173 @@ function columnNotes(c: IntrospectedColumn): string[] {
|
|
|
55
81
|
* relations at the top level, other schemas nested (`DB["auth"]["sessions"]`), views and
|
|
56
82
|
* materialized views as `View<…>`, enums under `$types`.
|
|
57
83
|
*/
|
|
58
|
-
export function generateSource(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
export function generateSource(
|
|
85
|
+
db: IntrospectedDatabase,
|
|
86
|
+
options: GenerateOptions = {},
|
|
87
|
+
): string {
|
|
88
|
+
const typeName = options.typeName ?? "DB";
|
|
89
|
+
const comments = options.comments ?? true;
|
|
90
|
+
const columnTypes = options.columnTypes ?? {};
|
|
91
|
+
const used = new Set<string>();
|
|
92
|
+
const usedKeys = new Set<string>();
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
/** The TS type `columnTypes` gives a column, if any. */
|
|
95
|
+
const customType = (
|
|
96
|
+
rel: IntrospectedRelation,
|
|
97
|
+
col: IntrospectedColumn,
|
|
98
|
+
): string | undefined => {
|
|
99
|
+
const qualified = `${rel.schema}.${rel.name}.${col.name}`;
|
|
100
|
+
const short = `${rel.name}.${col.name}`;
|
|
101
|
+
let ts: string | undefined;
|
|
102
|
+
if (typeof columnTypes === "function") ts = columnTypes(col, rel);
|
|
103
|
+
else {
|
|
104
|
+
const k =
|
|
105
|
+
qualified in columnTypes
|
|
106
|
+
? qualified
|
|
107
|
+
: rel.schema === "public" && short in columnTypes
|
|
108
|
+
? short
|
|
109
|
+
: undefined;
|
|
110
|
+
if (k !== undefined) {
|
|
111
|
+
usedKeys.add(k);
|
|
112
|
+
ts = columnTypes[k];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (ts !== undefined && col.type !== "json" && col.type !== "jsonb") {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`columnTypes: ${JSON.stringify(rel.schema === "public" ? short : qualified)} has type ${col.type}; columnTypes only types json / jsonb columns (use the column hook for others)`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return ts;
|
|
121
|
+
};
|
|
83
122
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
123
|
+
/** `Json<T>`, `Json<T, { nullable: true }>`, `Json<T, {}, "json">`, … */
|
|
124
|
+
const jsonDescriptor = (col: IntrospectedColumn, ts: string): string => {
|
|
125
|
+
const flags = [
|
|
126
|
+
...(col.nullable ? ["nullable: true"] : []),
|
|
127
|
+
...(col.hasDefault ? ["hasDefault: true"] : []),
|
|
128
|
+
];
|
|
129
|
+
const flagSource = flags.length > 0 ? `{ ${flags.join("; ")} }` : "{}";
|
|
130
|
+
used.add("Json");
|
|
131
|
+
if (col.type === "json") return `Json<${ts}, ${flagSource}, "json">`;
|
|
132
|
+
return flags.length > 0 ? `Json<${ts}, ${flagSource}>` : `Json<${ts}>`;
|
|
133
|
+
};
|
|
92
134
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
135
|
+
const relation = (rel: IntrospectedRelation, indent: string): string => {
|
|
136
|
+
const isView = rel.kind === "view" || rel.kind === "materialized view";
|
|
137
|
+
const cols = rel.columns.map((c) => {
|
|
138
|
+
let descriptor = options.column?.(c, rel);
|
|
139
|
+
if (descriptor === undefined) {
|
|
140
|
+
const custom = customType(rel, c);
|
|
141
|
+
if (custom !== undefined) descriptor = jsonDescriptor(c, custom);
|
|
142
|
+
else {
|
|
143
|
+
const helper = helperFor(c.nullable, c.hasDefault);
|
|
144
|
+
used.add(helper);
|
|
145
|
+
descriptor = `${helper}<${str(c.type)}>`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return `${comments ? doc(columnNotes(c), `${indent} `) : ""}${indent} ${key(c.name)}: ${descriptor};`;
|
|
149
|
+
});
|
|
150
|
+
const body = `{\n${cols.join("\n")}\n${indent}}`;
|
|
151
|
+
if (!isView) return body;
|
|
152
|
+
used.add("View");
|
|
153
|
+
return `View<${body}>`;
|
|
154
|
+
};
|
|
113
155
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
156
|
+
const relDoc = (rel: IntrospectedRelation, indent: string): string => {
|
|
157
|
+
if (!comments) return "";
|
|
158
|
+
const kind =
|
|
159
|
+
rel.kind === "table"
|
|
160
|
+
? []
|
|
161
|
+
: [`${rel.kind[0]!.toUpperCase()}${rel.kind.slice(1)}.`];
|
|
162
|
+
return doc([...(rel.comment ? [rel.comment] : []), ...kind], indent);
|
|
163
|
+
};
|
|
119
164
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
165
|
+
const topLevel = new Set(
|
|
166
|
+
db.relations.filter((r) => r.schema === "public").map((r) => r.name),
|
|
167
|
+
);
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
const namespaces = new Map<string, IntrospectedRelation[]>();
|
|
170
|
+
for (const rel of db.relations) {
|
|
171
|
+
if (rel.schema === "public")
|
|
172
|
+
lines.push(
|
|
173
|
+
`${relDoc(rel, " ")} ${key(rel.name)}: ${relation(rel, " ")};`,
|
|
174
|
+
);
|
|
175
|
+
else
|
|
176
|
+
namespaces.set(rel.schema, [...(namespaces.get(rel.schema) ?? []), rel]);
|
|
177
|
+
}
|
|
178
|
+
for (const [schema, rels] of namespaces) {
|
|
179
|
+
if (topLevel.has(schema)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`schema "${schema}" has the same name as the table "public.${schema}": TypeSQL keeps public relations and schemas in one object`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const body = rels.map(
|
|
185
|
+
(r) => `${relDoc(r, " ")} ${key(r.name)}: ${relation(r, " ")};`,
|
|
186
|
+
);
|
|
187
|
+
lines.push(` ${key(schema)}: {\n${body.join("\n")}\n };`);
|
|
188
|
+
}
|
|
134
189
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
190
|
+
if (db.enums.length > 0) {
|
|
191
|
+
// TypeSQL's enum registry is flat (`Col<"mood">`, schema qualifiers are dropped), so enums
|
|
192
|
+
// of the same name in different schemas share an entry.
|
|
193
|
+
const byName = new Map<
|
|
194
|
+
string,
|
|
195
|
+
{ values: Set<string>; schemas: string[] }
|
|
196
|
+
>();
|
|
197
|
+
for (const e of db.enums) {
|
|
198
|
+
const entry = byName.get(e.name) ?? {
|
|
199
|
+
values: new Set<string>(),
|
|
200
|
+
schemas: [],
|
|
201
|
+
};
|
|
202
|
+
for (const v of e.values) entry.values.add(v);
|
|
203
|
+
entry.schemas.push(e.schema);
|
|
204
|
+
byName.set(e.name, entry);
|
|
205
|
+
}
|
|
206
|
+
const entries = [...byName].map(([name, { values, schemas }]) => {
|
|
207
|
+
const notes =
|
|
208
|
+
comments && schemas.length > 1
|
|
209
|
+
? doc(
|
|
210
|
+
[
|
|
211
|
+
`Enums of the same name in ${schemas.map((s) => `\`${s}\``).join(", ")}.`,
|
|
212
|
+
],
|
|
213
|
+
" ",
|
|
214
|
+
)
|
|
215
|
+
: "";
|
|
216
|
+
return `${notes} ${key(name)}: ${[...values].map(str).join(" | ") || "never"};`;
|
|
217
|
+
});
|
|
218
|
+
lines.push(` $types: {\n${entries.join("\n")}\n };`);
|
|
219
|
+
}
|
|
151
220
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
221
|
+
const helpers = [
|
|
222
|
+
"Col",
|
|
223
|
+
"Generated",
|
|
224
|
+
"Json",
|
|
225
|
+
"Nullable",
|
|
226
|
+
"NullableGenerated",
|
|
227
|
+
"View",
|
|
228
|
+
].filter((h) => used.has(h));
|
|
229
|
+
const out = [
|
|
230
|
+
`// Generated by \`typesql introspect\` (@typesql/pg) from PostgreSQL ${db.version.split(/[.\s]/)[0]}. Do not edit.`,
|
|
231
|
+
...(helpers.length > 0
|
|
232
|
+
? [
|
|
233
|
+
`import type { ${helpers.join(", ")} } from ${str(options.importFrom ?? "@typesql/core")};`,
|
|
234
|
+
]
|
|
235
|
+
: []),
|
|
236
|
+
...(options.imports ?? []),
|
|
237
|
+
"",
|
|
238
|
+
`export interface ${key(typeName)} {`,
|
|
239
|
+
...lines,
|
|
240
|
+
"}",
|
|
241
|
+
"",
|
|
242
|
+
];
|
|
243
|
+
const unknownKeys =
|
|
244
|
+
typeof columnTypes === "function"
|
|
245
|
+
? []
|
|
246
|
+
: Object.keys(columnTypes).filter((k) => !usedKeys.has(k));
|
|
247
|
+
if (unknownKeys.length > 0 && options.column === undefined) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`columnTypes: no column ${unknownKeys.map((k) => JSON.stringify(k)).join(", ")} (keys are "table.column" for public tables, else "schema.table.column")`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return out.join("\n");
|
|
168
253
|
}
|