@typesql/pg 0.1.5 → 0.2.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/CHANGELOG.md +24 -0
- package/README.md +21 -59
- 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/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
|
}
|
package/src/introspect/index.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
export type { IntrospectConfig } from "./cli.js";
|
|
2
|
+
export { type GenerateOptions, generateSource } from "./emit.js";
|
|
1
3
|
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
type IntrospectedColumn,
|
|
5
|
+
type IntrospectedDatabase,
|
|
6
|
+
type IntrospectedEnum,
|
|
7
|
+
type IntrospectedRelation,
|
|
8
|
+
type IntrospectOptions,
|
|
9
|
+
introspect,
|
|
10
|
+
type RelationKind,
|
|
9
11
|
} from "./introspect.js";
|
|
10
|
-
export { generateSource, type GenerateOptions } from "./emit.js";
|
|
11
|
-
export { ViewNullability, parseNodeTree, type NullabilityOptions, type QueryFn } from "./nullability.js";
|
|
12
|
-
export { main, type IntrospectConfig } from "./cli.js";
|