@supabase/postgrest-typegen 0.1.0 → 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/README.md +4 -0
- package/dist/generation/go.js +30 -2
- package/dist/generation/python.js +24 -10
- package/dist/generation/swift.js +43 -1
- package/dist/generation/typescript.d.ts +7 -0
- package/dist/generation/typescript.js +37 -11
- package/dist/introspection/index.js +27 -21
- package/dist/introspection/sql/columns.sql.js +19 -2
- package/dist/introspection/sql/foreign_tables.sql.js +1 -1
- package/dist/introspection/sql/functions.sql.js +1 -1
- package/dist/introspection/sql/helpers.d.ts +1 -1
- package/dist/introspection/sql/helpers.js +2 -2
- package/dist/introspection/sql/materialized_views.sql.js +1 -1
- package/dist/introspection/sql/pg-format.d.ts +21 -0
- package/dist/introspection/sql/pg-format.js +92 -0
- package/dist/introspection/sql/primary_keys.sql.js +1 -1
- package/dist/introspection/sql/schemas.sql.js +1 -1
- package/dist/introspection/sql/table.sql.js +1 -1
- package/dist/introspection/sql/types.sql.js +1 -1
- package/dist/introspection/sql/views.sql.js +7 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.js +9 -0
- package/package.json +2 -5
- package/src/generation/go.ts +31 -2
- package/src/generation/python.ts +25 -10
- package/src/generation/swift.ts +44 -1
- package/src/generation/typescript.ts +53 -10
- package/src/introspection/index.ts +41 -38
- package/src/introspection/sql/columns.sql.ts +19 -2
- package/src/introspection/sql/foreign_tables.sql.ts +1 -1
- package/src/introspection/sql/functions.sql.ts +1 -1
- package/src/introspection/sql/helpers.ts +2 -2
- package/src/introspection/sql/materialized_views.sql.ts +1 -1
- package/src/introspection/sql/pg-format.ts +101 -0
- package/src/introspection/sql/primary_keys.sql.ts +1 -1
- package/src/introspection/sql/schemas.sql.ts +1 -1
- package/src/introspection/sql/table.sql.ts +1 -1
- package/src/introspection/sql/types.sql.ts +1 -1
- package/src/introspection/sql/views.sql.ts +7 -1
- package/src/types.ts +9 -0
package/dist/types.d.ts
CHANGED
|
@@ -182,6 +182,8 @@ declare const postgresViewSchema: import("arktype/internal/variants/object.ts").
|
|
|
182
182
|
schema: string;
|
|
183
183
|
name: string;
|
|
184
184
|
is_updatable: boolean;
|
|
185
|
+
is_insert_enabled: boolean;
|
|
186
|
+
is_update_enabled: boolean;
|
|
185
187
|
comment: string | null;
|
|
186
188
|
columns?: {
|
|
187
189
|
table_id: number;
|
|
@@ -250,7 +252,9 @@ export declare const generatorMetadataSchema: import("arktype/internal/variants/
|
|
|
250
252
|
views: {
|
|
251
253
|
comment: string | null;
|
|
252
254
|
id: number;
|
|
255
|
+
is_insert_enabled: boolean;
|
|
253
256
|
is_updatable: boolean;
|
|
257
|
+
is_update_enabled: boolean;
|
|
254
258
|
name: string;
|
|
255
259
|
schema: string;
|
|
256
260
|
}[];
|
package/dist/types.js
CHANGED
|
@@ -136,6 +136,15 @@ const postgresViewSchema = type({
|
|
|
136
136
|
schema: "string",
|
|
137
137
|
name: "string",
|
|
138
138
|
is_updatable: "boolean",
|
|
139
|
+
/**
|
|
140
|
+
* Whether INSERT works through this view: auto-updatable per
|
|
141
|
+
* `pg_relation_is_updatable`, or made insertable by an INSTEAD OF INSERT
|
|
142
|
+
* trigger. Tracked separately from `is_updatable` (which mirrors
|
|
143
|
+
* `information_schema.views` and ignores triggers).
|
|
144
|
+
*/
|
|
145
|
+
is_insert_enabled: "boolean",
|
|
146
|
+
/** Same as `is_insert_enabled`, for UPDATE. */
|
|
147
|
+
is_update_enabled: "boolean",
|
|
139
148
|
comment: "string | null",
|
|
140
149
|
"columns?": postgresColumnSchema.array(),
|
|
141
150
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/postgrest-typegen",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Type generation for PostgREST from PostgreSQL schemas",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codegen",
|
|
@@ -62,8 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"arktype": "2.2.3",
|
|
65
|
-
"
|
|
66
|
-
"prettier": "3.5.3"
|
|
65
|
+
"oxfmt": "0.65.0"
|
|
67
66
|
},
|
|
68
67
|
"devDependencies": {
|
|
69
68
|
"@testcontainers/postgresql": "^12.1.0",
|
|
@@ -72,9 +71,7 @@
|
|
|
72
71
|
"@types/bun": "^1.4.0",
|
|
73
72
|
"@types/node": "^26.3.0",
|
|
74
73
|
"@types/pg": "^8.23.1",
|
|
75
|
-
"@types/pg-format": "^1.0.5",
|
|
76
74
|
"knip": "^6.32.3",
|
|
77
|
-
"oxfmt": "0.65.0",
|
|
78
75
|
"oxlint": "1.80.0",
|
|
79
76
|
"oxlint-tsgolint": "7.0.2001",
|
|
80
77
|
"pg": "^8.23.0",
|
package/src/generation/go.ts
CHANGED
|
@@ -118,6 +118,35 @@ function formatForGoTypeName(name: string): string {
|
|
|
118
118
|
.join("");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Renders the `json` struct tag for a column or composite attribute name.
|
|
123
|
+
*
|
|
124
|
+
* The name is quoted with `JSON.stringify`, whose escape sequences are a
|
|
125
|
+
* subset of Go's, so `reflect.StructTag.Get("json")` recovers the exact name
|
|
126
|
+
* via `strconv.Unquote` even when it contains quotes, backslashes or control
|
|
127
|
+
* characters. The tag is emitted as a raw string literal like before, except
|
|
128
|
+
* when the name contains a backtick: Go raw literals cannot contain one, so
|
|
129
|
+
* such tags fall back to an interpreted literal with a second round of
|
|
130
|
+
* escaping. Names the marshaler cannot represent still compile and
|
|
131
|
+
* round-trip through `reflect.StructTag`, but `encoding/json` applies its
|
|
132
|
+
* own tag rules at runtime: a comma splits the name from tag options (so
|
|
133
|
+
* only the part before it is used as the JSON name), and a name it rejects
|
|
134
|
+
* outright (for example one containing a quote) falls back to the Go field
|
|
135
|
+
* name. That is a limitation of the struct tag convention itself, not of
|
|
136
|
+
* the generated source.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* formatForGoStructTag('name') // `json:"name"`
|
|
141
|
+
* formatForGoStructTag('a"b') // `json:"a\"b"`
|
|
142
|
+
* formatForGoStructTag('back`tick') // "json:\"back`tick\""
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
function formatForGoStructTag(name: string): string {
|
|
146
|
+
const tag = `json:${JSON.stringify(name)}`;
|
|
147
|
+
return tag.includes("`") ? JSON.stringify(tag) : `\`${tag}\``;
|
|
148
|
+
}
|
|
149
|
+
|
|
121
150
|
function generateTableStruct(
|
|
122
151
|
schema: PostgresSchema,
|
|
123
152
|
table: PostgresTable | PostgresView | PostgresMaterializedView,
|
|
@@ -170,7 +199,7 @@ function generateTableStruct(
|
|
|
170
199
|
([formattedName, type, name]) => {
|
|
171
200
|
return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(
|
|
172
201
|
maxTypeLength,
|
|
173
|
-
)}
|
|
202
|
+
)} ${formatForGoStructTag(name)}`;
|
|
174
203
|
},
|
|
175
204
|
);
|
|
176
205
|
|
|
@@ -232,7 +261,7 @@ function generateCompositeTypeStruct(
|
|
|
232
261
|
([formattedName, type, name]) => {
|
|
233
262
|
return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(
|
|
234
263
|
maxTypeLength,
|
|
235
|
-
)}
|
|
264
|
+
)} ${formatForGoStructTag(name)}`;
|
|
236
265
|
},
|
|
237
266
|
);
|
|
238
267
|
|
package/src/generation/python.ts
CHANGED
|
@@ -48,13 +48,12 @@ from typing import (
|
|
|
48
48
|
Any,
|
|
49
49
|
List,
|
|
50
50
|
Literal,
|
|
51
|
-
NotRequired,
|
|
52
51
|
Optional,
|
|
53
|
-
TypeAlias,
|
|
54
52
|
TypedDict,
|
|
55
53
|
)
|
|
54
|
+
from typing_extensions import NotRequired, TypeAlias
|
|
56
55
|
|
|
57
|
-
from pydantic import BaseModel, Field,
|
|
56
|
+
from pydantic import BaseModel, Field, JsonValue
|
|
58
57
|
|
|
59
58
|
${concatLines(Object.values(ctx.user_enums))}
|
|
60
59
|
|
|
@@ -143,7 +142,9 @@ class PythonContext {
|
|
|
143
142
|
const attributeEntries: PythonBaseModelAttr[] = attributes.map(
|
|
144
143
|
(attribute) => {
|
|
145
144
|
const type = this.parsePgType(attribute.type!.name);
|
|
146
|
-
|
|
145
|
+
// Composite type attributes cannot carry NOT NULL constraints in
|
|
146
|
+
// Postgres, so every field is inherently nullable.
|
|
147
|
+
return new PythonBaseModelAttr(attribute.name, type, true);
|
|
147
148
|
},
|
|
148
149
|
);
|
|
149
150
|
|
|
@@ -229,7 +230,9 @@ class PythonEnum implements Serializable {
|
|
|
229
230
|
this.variants = type.enums;
|
|
230
231
|
}
|
|
231
232
|
serialize(): string {
|
|
232
|
-
const variants = this.variants
|
|
233
|
+
const variants = this.variants
|
|
234
|
+
.map((item) => escapePythonString(item))
|
|
235
|
+
.join(", ");
|
|
233
236
|
return `${this.name}: TypeAlias = Literal[${variants}]`;
|
|
234
237
|
}
|
|
235
238
|
}
|
|
@@ -273,7 +276,7 @@ class PythonBaseModelAttr implements Serializable {
|
|
|
273
276
|
const py_type = this.nullable
|
|
274
277
|
? `Optional[${this.py_type.serialize()}]`
|
|
275
278
|
: this.py_type.serialize();
|
|
276
|
-
return ` ${this.name}: ${py_type} = Field(alias
|
|
279
|
+
return ` ${this.name}: ${py_type} = Field(alias=${escapePythonString(this.pg_name)})`;
|
|
277
280
|
}
|
|
278
281
|
}
|
|
279
282
|
|
|
@@ -326,7 +329,7 @@ class PythonTypedDictAttr implements Serializable {
|
|
|
326
329
|
const py_type = this.nullable
|
|
327
330
|
? `Optional[${this.py_type.serialize()}]`
|
|
328
331
|
: this.py_type.serialize();
|
|
329
|
-
const annotation = `Annotated[${py_type}, Field(alias
|
|
332
|
+
const annotation = `Annotated[${py_type}, Field(alias=${escapePythonString(this.pg_name)})]`;
|
|
330
333
|
const rhs = this.not_required ? `NotRequired[${annotation}]` : annotation;
|
|
331
334
|
return ` ${this.name}: ${rhs}`;
|
|
332
335
|
}
|
|
@@ -367,6 +370,16 @@ function concatLines(items: Serializable[]): string {
|
|
|
367
370
|
return items.map((item) => item.serialize()).join("\n\n");
|
|
368
371
|
}
|
|
369
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Emits a Postgres name as a double-quoted Python string literal. JSON string
|
|
375
|
+
* escaping is a subset of Python's (`\"`, `\\`, `\n`, `\uXXXX`), so
|
|
376
|
+
* `JSON.stringify` yields a literal Python parses to the exact original name
|
|
377
|
+
* and keeps quotes, backslashes and newlines from breaking out of it.
|
|
378
|
+
*/
|
|
379
|
+
function escapePythonString(value: string): string {
|
|
380
|
+
return JSON.stringify(value);
|
|
381
|
+
}
|
|
382
|
+
|
|
370
383
|
const PY_TYPE_MAP: Record<string, string> = {
|
|
371
384
|
// Bool
|
|
372
385
|
bool: "bool",
|
|
@@ -395,9 +408,11 @@ const PY_TYPE_MAP: Record<string, string> = {
|
|
|
395
408
|
vector: "list[Any]",
|
|
396
409
|
interval: "str",
|
|
397
410
|
|
|
398
|
-
// JSON
|
|
399
|
-
|
|
400
|
-
|
|
411
|
+
// JSON. PostgREST returns these columns already deserialized, so the
|
|
412
|
+
// generated models must accept parsed values (JsonValue), not JSON strings
|
|
413
|
+
// (which is what pydantic's Json[...] validates and parses).
|
|
414
|
+
json: "JsonValue",
|
|
415
|
+
jsonb: "JsonValue",
|
|
401
416
|
|
|
402
417
|
// Range types (can be adjusted to more complex types if needed)
|
|
403
418
|
int4range: "str",
|
package/src/generation/swift.ts
CHANGED
|
@@ -174,6 +174,49 @@ function generateProtocolConformances(protocols: string[]): string {
|
|
|
174
174
|
return protocols.length === 0 ? "" : `: ${protocols.join(", ")}`;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Serializes a database-provided value as a Swift string literal. Backslashes
|
|
179
|
+
* would otherwise start escape or interpolation sequences (`\(`), double
|
|
180
|
+
* quotes would terminate the literal early, and line breaks or control
|
|
181
|
+
* characters are not allowed in single-line literals, so all of them are
|
|
182
|
+
* escaped per Swift's string literal grammar. U+2028 and U+2029 are legal in
|
|
183
|
+
* Swift literals but are escaped anyway because other tooling mishandles
|
|
184
|
+
* them. Swift unescapes at compile time, so raw values round-trip unchanged.
|
|
185
|
+
*/
|
|
186
|
+
function swiftStringLiteral(value: string): string {
|
|
187
|
+
let escaped = "";
|
|
188
|
+
for (const character of value) {
|
|
189
|
+
switch (character) {
|
|
190
|
+
case "\\":
|
|
191
|
+
escaped += "\\\\";
|
|
192
|
+
break;
|
|
193
|
+
case '"':
|
|
194
|
+
escaped += '\\"';
|
|
195
|
+
break;
|
|
196
|
+
case "\t":
|
|
197
|
+
escaped += "\\t";
|
|
198
|
+
break;
|
|
199
|
+
case "\n":
|
|
200
|
+
escaped += "\\n";
|
|
201
|
+
break;
|
|
202
|
+
case "\r":
|
|
203
|
+
escaped += "\\r";
|
|
204
|
+
break;
|
|
205
|
+
default: {
|
|
206
|
+
const codePoint = character.codePointAt(0)!;
|
|
207
|
+
escaped +=
|
|
208
|
+
codePoint < 0x20 ||
|
|
209
|
+
codePoint === 0x7f ||
|
|
210
|
+
codePoint === 0x2028 ||
|
|
211
|
+
codePoint === 0x2029
|
|
212
|
+
? `\\u{${codePoint.toString(16)}}`
|
|
213
|
+
: character;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return `"${escaped}"`;
|
|
218
|
+
}
|
|
219
|
+
|
|
177
220
|
function generateEnum(
|
|
178
221
|
enum_: SwiftEnum,
|
|
179
222
|
{ accessControl, level }: SwiftGeneratorOptions & { level: number },
|
|
@@ -184,7 +227,7 @@ function generateEnum(
|
|
|
184
227
|
)} {`,
|
|
185
228
|
...enum_.cases.map(
|
|
186
229
|
(case_) =>
|
|
187
|
-
`${ident(level + 1)}case ${case_.formattedName} =
|
|
230
|
+
`${ident(level + 1)}case ${case_.formattedName} = ${swiftStringLiteral(case_.rawValue)}`,
|
|
188
231
|
),
|
|
189
232
|
`${ident(level)}}`,
|
|
190
233
|
];
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// literals (relying on Array#toString) to build the output. Preserving that is required for
|
|
4
4
|
// byte-parity, and the offending expressions sit inside template literals where a targeted
|
|
5
5
|
// disable comment would corrupt the emitted string.
|
|
6
|
-
import
|
|
6
|
+
import { format as oxfmtFormat } from "oxfmt";
|
|
7
7
|
import type {
|
|
8
8
|
GeneratorMetadata,
|
|
9
9
|
PostgresColumn,
|
|
@@ -35,8 +35,30 @@ export interface GenerateTypescriptOptions {
|
|
|
35
35
|
* env read. Default `'public'`.
|
|
36
36
|
*/
|
|
37
37
|
defaultSchema?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Formatter used on the generated output. Defaults to `oxfmt`, called
|
|
40
|
+
* inline. Callers on a latency-sensitive request path (e.g. postgres-meta's
|
|
41
|
+
* hosted `/generators/typescript` route) can substitute a worker-pool-backed
|
|
42
|
+
* formatter here instead of blocking the event loop on every call.
|
|
43
|
+
*/
|
|
44
|
+
format?: (code: string) => Promise<string>;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
const defaultFormat = async (code: string): Promise<string> => {
|
|
48
|
+
const { code: formatted, errors } = await oxfmtFormat("output.ts", code, {
|
|
49
|
+
semi: false,
|
|
50
|
+
printWidth: 80,
|
|
51
|
+
});
|
|
52
|
+
if (errors.length > 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`oxfmt failed to format generated TypeScript output: ${errors
|
|
55
|
+
.map((error) => error.message)
|
|
56
|
+
.join("; ")}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return formatted;
|
|
60
|
+
};
|
|
61
|
+
|
|
40
62
|
type TsRelationship = Pick<
|
|
41
63
|
GeneratorMetadata["relationships"][number],
|
|
42
64
|
| "foreign_key_name"
|
|
@@ -71,6 +93,7 @@ export const generateTypescript = async (
|
|
|
71
93
|
detectOneToOneRelationships = false,
|
|
72
94
|
postgrestVersion,
|
|
73
95
|
defaultSchema = "public",
|
|
96
|
+
format = defaultFormat,
|
|
74
97
|
} = opts;
|
|
75
98
|
// Ordering of all collections (incl. relationships, columns, args below) is
|
|
76
99
|
// provided by `sortGeneratorMetadata`; this generator no longer re-sorts.
|
|
@@ -209,6 +232,8 @@ export const generateTypescript = async (
|
|
|
209
232
|
view: {
|
|
210
233
|
...materializedView,
|
|
211
234
|
is_updatable: false,
|
|
235
|
+
is_insert_enabled: false,
|
|
236
|
+
is_update_enabled: false,
|
|
212
237
|
},
|
|
213
238
|
relationships: getRelationships(materializedView),
|
|
214
239
|
});
|
|
@@ -524,7 +549,13 @@ export const generateTypescript = async (
|
|
|
524
549
|
) => {
|
|
525
550
|
return fns
|
|
526
551
|
.map(({ fn, inArgs }) => {
|
|
527
|
-
|
|
552
|
+
// `never` would be wrong here: postgrest-js treats `Args extends { '': Row }`
|
|
553
|
+
// as a computed field (`never extends T` is always true, so same-named table
|
|
554
|
+
// columns get omitted from select results), and an object type with a required
|
|
555
|
+
// `never` property is uninhabited for any tool doing sound type math on the
|
|
556
|
+
// generated types. `Record<PropertyKey, never>` is the accurate type for
|
|
557
|
+
// "callable with no arguments".
|
|
558
|
+
let argsType = "Record<PropertyKey, never>";
|
|
528
559
|
let returnType = getFunctionReturnType(schema, fn);
|
|
529
560
|
|
|
530
561
|
// Check for specific error cases
|
|
@@ -614,6 +645,11 @@ export const generateTypescript = async (
|
|
|
614
645
|
: "";
|
|
615
646
|
|
|
616
647
|
function generateNullableUnionTsType(tsType: string, isNullable: boolean) {
|
|
648
|
+
// The generated Json type includes null, so a NOT NULL json/jsonb column
|
|
649
|
+
// has to be narrowed with NonNullable to reflect the database constraint.
|
|
650
|
+
if (tsType === "Json" && !isNullable) {
|
|
651
|
+
return `NonNullable<${tsType}>`;
|
|
652
|
+
}
|
|
617
653
|
// Only add the null union if the type is not unknown as unknown already includes null
|
|
618
654
|
if (tsType === "unknown" || tsType === "any" || !isNullable) {
|
|
619
655
|
return tsType;
|
|
@@ -693,7 +729,10 @@ export type Database = {
|
|
|
693
729
|
}
|
|
694
730
|
Insert: {
|
|
695
731
|
${columnsByTableId[table.id].map((column) => {
|
|
696
|
-
if (
|
|
732
|
+
if (
|
|
733
|
+
column.identity_generation === "ALWAYS" ||
|
|
734
|
+
column.is_generated
|
|
735
|
+
) {
|
|
697
736
|
return `${JSON.stringify(column.name)}?: never`;
|
|
698
737
|
}
|
|
699
738
|
return generateColumnTsDefinition(
|
|
@@ -714,7 +753,10 @@ export type Database = {
|
|
|
714
753
|
}
|
|
715
754
|
Update: {
|
|
716
755
|
${columnsByTableId[table.id].map((column) => {
|
|
717
|
-
if (
|
|
756
|
+
if (
|
|
757
|
+
column.identity_generation === "ALWAYS" ||
|
|
758
|
+
column.is_generated
|
|
759
|
+
) {
|
|
718
760
|
return `${JSON.stringify(column.name)}?: never`;
|
|
719
761
|
}
|
|
720
762
|
|
|
@@ -774,7 +816,7 @@ export type Database = {
|
|
|
774
816
|
]}
|
|
775
817
|
}
|
|
776
818
|
${
|
|
777
|
-
view.
|
|
819
|
+
view.is_insert_enabled
|
|
778
820
|
? `Insert: {
|
|
779
821
|
${columnsByTableId[view.id].map((column) => {
|
|
780
822
|
if (!column.is_updatable) {
|
|
@@ -793,7 +835,11 @@ export type Database = {
|
|
|
793
835
|
);
|
|
794
836
|
})}
|
|
795
837
|
}
|
|
796
|
-
|
|
838
|
+
`
|
|
839
|
+
: ""
|
|
840
|
+
}${
|
|
841
|
+
view.is_update_enabled
|
|
842
|
+
? `Update: {
|
|
797
843
|
${columnsByTableId[view.id].map((column) => {
|
|
798
844
|
if (!column.is_updatable) {
|
|
799
845
|
return `${JSON.stringify(column.name)}?: never`;
|
|
@@ -1016,10 +1062,7 @@ export const Constants = {
|
|
|
1016
1062
|
} as const
|
|
1017
1063
|
`;
|
|
1018
1064
|
|
|
1019
|
-
output = await
|
|
1020
|
-
parser: "typescript",
|
|
1021
|
-
semi: false,
|
|
1022
|
-
});
|
|
1065
|
+
output = await format(output);
|
|
1023
1066
|
return output;
|
|
1024
1067
|
};
|
|
1025
1068
|
|
|
@@ -62,45 +62,48 @@ export async function introspect(
|
|
|
62
62
|
const queryRows = async (sql: string) =>
|
|
63
63
|
normalizeRows((await db.query(sql)).rows);
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// types: includeSystemSchemas true (no schema filter), table + array types included.
|
|
97
|
-
const types = await queryRows(
|
|
98
|
-
TYPES_SQL({
|
|
99
|
-
schemaFilter: "",
|
|
100
|
-
includeTableTypes: true,
|
|
101
|
-
includeArrayTypes: true,
|
|
65
|
+
// Issued concurrently, matching postgres-meta's CLI path which ran its nine
|
|
66
|
+
// introspection queries under Promise.all. A pooled Queryable runs them in
|
|
67
|
+
// parallel; a single-connection one pipelines them, which is no worse than
|
|
68
|
+
// awaiting each in turn.
|
|
69
|
+
const [
|
|
70
|
+
schemas,
|
|
71
|
+
tables,
|
|
72
|
+
foreignTables,
|
|
73
|
+
views,
|
|
74
|
+
materializedViews,
|
|
75
|
+
columns,
|
|
76
|
+
primaryKeys,
|
|
77
|
+
relationships,
|
|
78
|
+
functions,
|
|
79
|
+
types,
|
|
80
|
+
] = await Promise.all([
|
|
81
|
+
queryRows(
|
|
82
|
+
SCHEMAS_SQL({
|
|
83
|
+
includeSystemSchemas: false,
|
|
84
|
+
nameFilter: systemExcludingFilter,
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
queryRows(TABLES_SQL({ schemaFilter: systemExcludingFilter })),
|
|
88
|
+
queryRows(FOREIGN_TABLES_SQL({ schemaFilter: plainFilter })),
|
|
89
|
+
queryRows(VIEWS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
90
|
+
queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
|
|
91
|
+
queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
92
|
+
queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
93
|
+
listRelationships(db, {
|
|
94
|
+
includedSchemas: included,
|
|
95
|
+
excludedSchemas: excluded,
|
|
102
96
|
}),
|
|
103
|
-
|
|
97
|
+
queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
98
|
+
// types: includeSystemSchemas true (no schema filter), table + array types included.
|
|
99
|
+
queryRows(
|
|
100
|
+
TYPES_SQL({
|
|
101
|
+
schemaFilter: "",
|
|
102
|
+
includeTableTypes: true,
|
|
103
|
+
includeArrayTypes: true,
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
]);
|
|
104
107
|
|
|
105
108
|
return {
|
|
106
109
|
version: GENERATOR_METADATA_VERSION,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { literal } from "pg-format";
|
|
1
|
+
import { literal } from "./pg-format.ts";
|
|
2
2
|
import type { SQLQueryPropsWithSchemaFilter } from "./common.ts";
|
|
3
3
|
|
|
4
4
|
export const COLUMNS_SQL = (
|
|
@@ -52,7 +52,24 @@ SELECT
|
|
|
52
52
|
) AS is_nullable,
|
|
53
53
|
(
|
|
54
54
|
c.relkind IN ('r', 'p')
|
|
55
|
-
|
|
55
|
+
-- Columns of views made writable by INSTEAD OF triggers can be written
|
|
56
|
+
-- through PostgREST even though the view is not auto-updatable, so they
|
|
57
|
+
-- must not degrade to \`?: never\` in generated Insert/Update types.
|
|
58
|
+
-- include_triggers => TRUE covers INSTEAD OF UPDATE triggers, but
|
|
59
|
+
-- pg_column_is_updatable only considers the UPDATE event, so INSTEAD OF
|
|
60
|
+
-- INSERT triggers (tgtype bits: 64 = INSTEAD, 4 = INSERT) are checked
|
|
61
|
+
-- separately. Which events a view accepts is gated per view via
|
|
62
|
+
-- is_insert_enabled / is_update_enabled.
|
|
63
|
+
OR c.relkind IN ('v', 'f') AND (
|
|
64
|
+
pg_column_is_updatable(c.oid, a.attnum, TRUE)
|
|
65
|
+
OR EXISTS (
|
|
66
|
+
SELECT 1 FROM pg_trigger tg
|
|
67
|
+
WHERE tg.tgrelid = c.oid
|
|
68
|
+
AND tg.tgtype & 64 <> 0
|
|
69
|
+
AND tg.tgtype & 4 <> 0
|
|
70
|
+
AND NOT tg.tgisinternal
|
|
71
|
+
)
|
|
72
|
+
)
|
|
56
73
|
) AS is_updatable,
|
|
57
74
|
uniques.table_id IS NOT NULL AS is_unique,
|
|
58
75
|
check_constraints.definition AS "check",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// oxlint-disable typescript/restrict-template-expressions -- verbatim port of postgres-meta's
|
|
2
2
|
// builder, which interpolates the `args` string[] into the SQL (relying on Array#toString). The
|
|
3
3
|
// expression sits inside a template literal where a targeted disable comment would corrupt the SQL.
|
|
4
|
-
import { literal } from "pg-format";
|
|
4
|
+
import { literal } from "./pg-format.ts";
|
|
5
5
|
import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
|
|
6
6
|
|
|
7
7
|
export const FUNCTIONS_SQL = (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { literal } from "pg-format";
|
|
1
|
+
import { literal } from "./pg-format.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Schemas that postgres-meta excludes by default when `includeSystemSchemas`
|
|
@@ -13,7 +13,7 @@ export const DEFAULT_SYSTEM_SCHEMAS = [
|
|
|
13
13
|
/**
|
|
14
14
|
* Build a SQL `IN (...)` / `NOT IN (...)` fragment for an include/exclude list,
|
|
15
15
|
* ported verbatim from `postgres-meta/src/lib/helpers.ts`. Values are escaped
|
|
16
|
-
* with pg-format
|
|
16
|
+
* with the inlined pg-format `literal`. Returns an empty string when there is nothing to
|
|
17
17
|
* filter, so callers interpolate it conditionally.
|
|
18
18
|
*/
|
|
19
19
|
export const filterByList = (
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal inline port of the `pg-format` helpers this package uses, so the
|
|
3
|
+
* package carries no runtime dependency for them (the CommonJS-only
|
|
4
|
+
* `pg-format` package is awkward to consume from ESM/bundled CLIs).
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `pg-format@1.0.4` byte-for-byte for the inputs postgres-meta feeds
|
|
7
|
+
* it — in particular numbers are quoted (`literal(10)` is `'10'`), because
|
|
8
|
+
* that is what postgres-meta's SQL emits and this package is byte-parity
|
|
9
|
+
* constrained against it. The `supabase/supabase` `packages/pg-meta` port
|
|
10
|
+
* deliberately diverges here (unquoted numbers); do not "fix" this to match it.
|
|
11
|
+
*
|
|
12
|
+
* Originally ported from PostgreSQL's `src/interfaces/libpq/fe-exec.c`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Convert a JS ISO string to Postgres' default ISO 8601 form. */
|
|
16
|
+
function formatDate(date: string): string {
|
|
17
|
+
return date.replace("T", " ").replace("Z", "+00");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function arrayToList(
|
|
21
|
+
useSpace: boolean,
|
|
22
|
+
array: unknown[],
|
|
23
|
+
formatter: (value: unknown) => string,
|
|
24
|
+
): string {
|
|
25
|
+
let sql = useSpace ? " (" : "(";
|
|
26
|
+
for (const [index, element] of array.entries()) {
|
|
27
|
+
sql += (index === 0 ? "" : ", ") + formatter(element);
|
|
28
|
+
}
|
|
29
|
+
sql += ")";
|
|
30
|
+
return sql;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Quote a value as a SQL literal. `null`/`undefined` become `NULL`, booleans
|
|
35
|
+
* `'t'`/`'f'`, dates ISO 8601 strings, arrays comma-separated lists (nested
|
|
36
|
+
* arrays become parenthesised groups), plain objects `'…'::jsonb`, and
|
|
37
|
+
* everything else — including numbers — a single-quoted string with embedded
|
|
38
|
+
* quotes doubled. Strings containing a backslash use the `E'…'` form.
|
|
39
|
+
*/
|
|
40
|
+
export function literal(value?: unknown): string {
|
|
41
|
+
let text: string;
|
|
42
|
+
let explicitCast: string | undefined;
|
|
43
|
+
|
|
44
|
+
if (value === undefined || value === null) {
|
|
45
|
+
return "NULL";
|
|
46
|
+
}
|
|
47
|
+
if (value === false) {
|
|
48
|
+
return "'f'";
|
|
49
|
+
}
|
|
50
|
+
if (value === true) {
|
|
51
|
+
return "'t'";
|
|
52
|
+
}
|
|
53
|
+
if (value instanceof Date) {
|
|
54
|
+
return `'${formatDate(value.toISOString())}'`;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
const parts: string[] = [];
|
|
58
|
+
for (const [index, element] of value.entries()) {
|
|
59
|
+
if (Array.isArray(element)) {
|
|
60
|
+
parts.push(arrayToList(index !== 0, element, literal));
|
|
61
|
+
} else {
|
|
62
|
+
parts.push(literal(element));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return parts.toString();
|
|
66
|
+
}
|
|
67
|
+
if (typeof value === "string") {
|
|
68
|
+
text = value;
|
|
69
|
+
} else if (typeof value === "number" || typeof value === "bigint") {
|
|
70
|
+
text = value.toString();
|
|
71
|
+
} else {
|
|
72
|
+
explicitCast = "jsonb";
|
|
73
|
+
text = JSON.stringify(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let hasBackslash = false;
|
|
77
|
+
let quoted = "'";
|
|
78
|
+
|
|
79
|
+
for (const c of text) {
|
|
80
|
+
if (c === "'") {
|
|
81
|
+
quoted += c + c;
|
|
82
|
+
} else if (c === "\\") {
|
|
83
|
+
quoted += c + c;
|
|
84
|
+
hasBackslash = true;
|
|
85
|
+
} else {
|
|
86
|
+
quoted += c;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
quoted += "'";
|
|
91
|
+
|
|
92
|
+
if (hasBackslash) {
|
|
93
|
+
quoted = `E${quoted}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (explicitCast) {
|
|
97
|
+
quoted += `::${explicitCast}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return quoted;
|
|
101
|
+
}
|