@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/README.md
CHANGED
|
@@ -128,6 +128,10 @@ Not yet published; consumed in-repo for now (`packages/postgrest-typegen`).
|
|
|
128
128
|
npm install pg
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
## Releasing
|
|
132
|
+
|
|
133
|
+
Unlike the rest of this repo, merging a change here does not open a release pull request on its own. Releases are cut on demand: run the `Release postgrest-typegen` workflow from the Actions tab, which runs release-please against `release-please-config.postgrest-typegen.json` and opens (or updates) the release pull request for this package. Merging that pull request tags the release and publishes to npm.
|
|
134
|
+
|
|
131
135
|
## License
|
|
132
136
|
|
|
133
137
|
MIT
|
package/dist/generation/go.js
CHANGED
|
@@ -58,6 +58,34 @@ function formatForGoTypeName(name) {
|
|
|
58
58
|
})
|
|
59
59
|
.join("");
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Renders the `json` struct tag for a column or composite attribute name.
|
|
63
|
+
*
|
|
64
|
+
* The name is quoted with `JSON.stringify`, whose escape sequences are a
|
|
65
|
+
* subset of Go's, so `reflect.StructTag.Get("json")` recovers the exact name
|
|
66
|
+
* via `strconv.Unquote` even when it contains quotes, backslashes or control
|
|
67
|
+
* characters. The tag is emitted as a raw string literal like before, except
|
|
68
|
+
* when the name contains a backtick: Go raw literals cannot contain one, so
|
|
69
|
+
* such tags fall back to an interpreted literal with a second round of
|
|
70
|
+
* escaping. Names the marshaler cannot represent still compile and
|
|
71
|
+
* round-trip through `reflect.StructTag`, but `encoding/json` applies its
|
|
72
|
+
* own tag rules at runtime: a comma splits the name from tag options (so
|
|
73
|
+
* only the part before it is used as the JSON name), and a name it rejects
|
|
74
|
+
* outright (for example one containing a quote) falls back to the Go field
|
|
75
|
+
* name. That is a limitation of the struct tag convention itself, not of
|
|
76
|
+
* the generated source.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* formatForGoStructTag('name') // `json:"name"`
|
|
81
|
+
* formatForGoStructTag('a"b') // `json:"a\"b"`
|
|
82
|
+
* formatForGoStructTag('back`tick') // "json:\"back`tick\""
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
function formatForGoStructTag(name) {
|
|
86
|
+
const tag = `json:${JSON.stringify(name)}`;
|
|
87
|
+
return tag.includes("`") ? JSON.stringify(tag) : `\`${tag}\``;
|
|
88
|
+
}
|
|
61
89
|
function generateTableStruct(schema, table, columns, types, operation) {
|
|
62
90
|
// Storing columns as a tuple of [formattedName, type, name] rather than creating the string
|
|
63
91
|
// representation of the line allows us to pre-format the entries. Go formats
|
|
@@ -97,7 +125,7 @@ function generateTableStruct(schema, table, columns, types, operation) {
|
|
|
97
125
|
// Pad the formatted name and type to align the struct fields, then join
|
|
98
126
|
// create the final string representation of the struct fields.
|
|
99
127
|
const formattedColumnEntries = columnEntries.map(([formattedName, type, name]) => {
|
|
100
|
-
return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(maxTypeLength)}
|
|
128
|
+
return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(maxTypeLength)} ${formatForGoStructTag(name)}`;
|
|
101
129
|
});
|
|
102
130
|
return `
|
|
103
131
|
type ${formatForGoTypeName(schema.name)}${formatForGoTypeName(table.name)}${operation} struct {
|
|
@@ -134,7 +162,7 @@ function generateCompositeTypeStruct(schema, type, types) {
|
|
|
134
162
|
// Pad the formatted name and type to align the struct fields, then join
|
|
135
163
|
// create the final string representation of the struct fields.
|
|
136
164
|
const formattedAttributeEntries = attributeEntries.map(([formattedName, type, name]) => {
|
|
137
|
-
return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(maxTypeLength)}
|
|
165
|
+
return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd(maxTypeLength)} ${formatForGoStructTag(name)}`;
|
|
138
166
|
});
|
|
139
167
|
return `
|
|
140
168
|
type ${formatForGoTypeName(schema.name)}${formatForGoTypeName(type.name)} struct {
|
|
@@ -26,13 +26,12 @@ from typing import (
|
|
|
26
26
|
Any,
|
|
27
27
|
List,
|
|
28
28
|
Literal,
|
|
29
|
-
NotRequired,
|
|
30
29
|
Optional,
|
|
31
|
-
TypeAlias,
|
|
32
30
|
TypedDict,
|
|
33
31
|
)
|
|
32
|
+
from typing_extensions import NotRequired, TypeAlias
|
|
34
33
|
|
|
35
|
-
from pydantic import BaseModel, Field,
|
|
34
|
+
from pydantic import BaseModel, Field, JsonValue
|
|
36
35
|
|
|
37
36
|
${concatLines(Object.values(ctx.user_enums))}
|
|
38
37
|
|
|
@@ -100,7 +99,9 @@ class PythonContext {
|
|
|
100
99
|
});
|
|
101
100
|
const attributeEntries = attributes.map((attribute) => {
|
|
102
101
|
const type = this.parsePgType(attribute.type.name);
|
|
103
|
-
|
|
102
|
+
// Composite type attributes cannot carry NOT NULL constraints in
|
|
103
|
+
// Postgres, so every field is inherently nullable.
|
|
104
|
+
return new PythonBaseModelAttr(attribute.name, type, true);
|
|
104
105
|
});
|
|
105
106
|
const schema = this.schemas[type.schema];
|
|
106
107
|
return new PythonBaseModel(type.name, schema, attributeEntries);
|
|
@@ -146,7 +147,9 @@ class PythonEnum {
|
|
|
146
147
|
this.variants = type.enums;
|
|
147
148
|
}
|
|
148
149
|
serialize() {
|
|
149
|
-
const variants = this.variants
|
|
150
|
+
const variants = this.variants
|
|
151
|
+
.map((item) => escapePythonString(item))
|
|
152
|
+
.join(", ");
|
|
150
153
|
return `${this.name}: TypeAlias = Literal[${variants}]`;
|
|
151
154
|
}
|
|
152
155
|
}
|
|
@@ -183,7 +186,7 @@ class PythonBaseModelAttr {
|
|
|
183
186
|
const py_type = this.nullable
|
|
184
187
|
? `Optional[${this.py_type.serialize()}]`
|
|
185
188
|
: this.py_type.serialize();
|
|
186
|
-
return ` ${this.name}: ${py_type} = Field(alias
|
|
189
|
+
return ` ${this.name}: ${py_type} = Field(alias=${escapePythonString(this.pg_name)})`;
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
class PythonBaseModel {
|
|
@@ -221,7 +224,7 @@ class PythonTypedDictAttr {
|
|
|
221
224
|
const py_type = this.nullable
|
|
222
225
|
? `Optional[${this.py_type.serialize()}]`
|
|
223
226
|
: this.py_type.serialize();
|
|
224
|
-
const annotation = `Annotated[${py_type}, Field(alias
|
|
227
|
+
const annotation = `Annotated[${py_type}, Field(alias=${escapePythonString(this.pg_name)})]`;
|
|
225
228
|
const rhs = this.not_required ? `NotRequired[${annotation}]` : annotation;
|
|
226
229
|
return ` ${this.name}: ${rhs}`;
|
|
227
230
|
}
|
|
@@ -251,6 +254,15 @@ class PythonTypedDict {
|
|
|
251
254
|
function concatLines(items) {
|
|
252
255
|
return items.map((item) => item.serialize()).join("\n\n");
|
|
253
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Emits a Postgres name as a double-quoted Python string literal. JSON string
|
|
259
|
+
* escaping is a subset of Python's (`\"`, `\\`, `\n`, `\uXXXX`), so
|
|
260
|
+
* `JSON.stringify` yields a literal Python parses to the exact original name
|
|
261
|
+
* and keeps quotes, backslashes and newlines from breaking out of it.
|
|
262
|
+
*/
|
|
263
|
+
function escapePythonString(value) {
|
|
264
|
+
return JSON.stringify(value);
|
|
265
|
+
}
|
|
254
266
|
const PY_TYPE_MAP = {
|
|
255
267
|
// Bool
|
|
256
268
|
bool: "bool",
|
|
@@ -276,9 +288,11 @@ const PY_TYPE_MAP = {
|
|
|
276
288
|
uuid: "uuid.UUID",
|
|
277
289
|
vector: "list[Any]",
|
|
278
290
|
interval: "str",
|
|
279
|
-
// JSON
|
|
280
|
-
|
|
281
|
-
|
|
291
|
+
// JSON. PostgREST returns these columns already deserialized, so the
|
|
292
|
+
// generated models must accept parsed values (JsonValue), not JSON strings
|
|
293
|
+
// (which is what pydantic's Json[...] validates and parses).
|
|
294
|
+
json: "JsonValue",
|
|
295
|
+
jsonb: "JsonValue",
|
|
282
296
|
// Range types (can be adjusted to more complex types if needed)
|
|
283
297
|
int4range: "str",
|
|
284
298
|
int4multirange: "str",
|
package/dist/generation/swift.js
CHANGED
|
@@ -94,10 +94,52 @@ function pgCompositeTypeToSwiftStruct(type, { types, views, tables, }) {
|
|
|
94
94
|
function generateProtocolConformances(protocols) {
|
|
95
95
|
return protocols.length === 0 ? "" : `: ${protocols.join(", ")}`;
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Serializes a database-provided value as a Swift string literal. Backslashes
|
|
99
|
+
* would otherwise start escape or interpolation sequences (`\(`), double
|
|
100
|
+
* quotes would terminate the literal early, and line breaks or control
|
|
101
|
+
* characters are not allowed in single-line literals, so all of them are
|
|
102
|
+
* escaped per Swift's string literal grammar. U+2028 and U+2029 are legal in
|
|
103
|
+
* Swift literals but are escaped anyway because other tooling mishandles
|
|
104
|
+
* them. Swift unescapes at compile time, so raw values round-trip unchanged.
|
|
105
|
+
*/
|
|
106
|
+
function swiftStringLiteral(value) {
|
|
107
|
+
let escaped = "";
|
|
108
|
+
for (const character of value) {
|
|
109
|
+
switch (character) {
|
|
110
|
+
case "\\":
|
|
111
|
+
escaped += "\\\\";
|
|
112
|
+
break;
|
|
113
|
+
case '"':
|
|
114
|
+
escaped += '\\"';
|
|
115
|
+
break;
|
|
116
|
+
case "\t":
|
|
117
|
+
escaped += "\\t";
|
|
118
|
+
break;
|
|
119
|
+
case "\n":
|
|
120
|
+
escaped += "\\n";
|
|
121
|
+
break;
|
|
122
|
+
case "\r":
|
|
123
|
+
escaped += "\\r";
|
|
124
|
+
break;
|
|
125
|
+
default: {
|
|
126
|
+
const codePoint = character.codePointAt(0);
|
|
127
|
+
escaped +=
|
|
128
|
+
codePoint < 0x20 ||
|
|
129
|
+
codePoint === 0x7f ||
|
|
130
|
+
codePoint === 0x2028 ||
|
|
131
|
+
codePoint === 0x2029
|
|
132
|
+
? `\\u{${codePoint.toString(16)}}`
|
|
133
|
+
: character;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return `"${escaped}"`;
|
|
138
|
+
}
|
|
97
139
|
function generateEnum(enum_, { accessControl, level }) {
|
|
98
140
|
return [
|
|
99
141
|
`${ident(level)}${accessControl} enum ${enum_.formattedEnumName}${generateProtocolConformances(enum_.protocolConformances)} {`,
|
|
100
|
-
...enum_.cases.map((case_) => `${ident(level + 1)}case ${case_.formattedName} =
|
|
142
|
+
...enum_.cases.map((case_) => `${ident(level + 1)}case ${case_.formattedName} = ${swiftStringLiteral(case_.rawValue)}`),
|
|
101
143
|
`${ident(level)}}`,
|
|
102
144
|
];
|
|
103
145
|
}
|
|
@@ -16,6 +16,13 @@ export interface GenerateTypescriptOptions {
|
|
|
16
16
|
* env read. Default `'public'`.
|
|
17
17
|
*/
|
|
18
18
|
defaultSchema?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Formatter used on the generated output. Defaults to `oxfmt`, called
|
|
21
|
+
* inline. Callers on a latency-sensitive request path (e.g. postgres-meta's
|
|
22
|
+
* hosted `/generators/typescript` route) can substitute a worker-pool-backed
|
|
23
|
+
* formatter here instead of blocking the event loop on every call.
|
|
24
|
+
*/
|
|
25
|
+
format?: (code: string) => Promise<string>;
|
|
19
26
|
}
|
|
20
27
|
/**
|
|
21
28
|
* Does not re-sort collections; pass input pre-sorted with
|
|
@@ -3,8 +3,20 @@
|
|
|
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 { VALID_FUNCTION_ARGS_MODE, VALID_UNNAMED_FUNCTION_ARG_TYPES, } from "./constants.js";
|
|
8
|
+
const defaultFormat = async (code) => {
|
|
9
|
+
const { code: formatted, errors } = await oxfmtFormat("output.ts", code, {
|
|
10
|
+
semi: false,
|
|
11
|
+
printWidth: 80,
|
|
12
|
+
});
|
|
13
|
+
if (errors.length > 0) {
|
|
14
|
+
throw new Error(`oxfmt failed to format generated TypeScript output: ${errors
|
|
15
|
+
.map((error) => error.message)
|
|
16
|
+
.join("; ")}`);
|
|
17
|
+
}
|
|
18
|
+
return formatted;
|
|
19
|
+
};
|
|
8
20
|
/**
|
|
9
21
|
* Does not re-sort collections; pass input pre-sorted with
|
|
10
22
|
* `sortGeneratorMetadata` first. Still merge-sorts tables+foreign tables and
|
|
@@ -13,7 +25,7 @@ import { VALID_FUNCTION_ARGS_MODE, VALID_UNNAMED_FUNCTION_ARG_TYPES, } from "./c
|
|
|
13
25
|
*/
|
|
14
26
|
export const generateTypescript = async (metadata, opts = {}) => {
|
|
15
27
|
const { schemas, tables, foreignTables, views, materializedViews, columns, relationships, functions, types, } = metadata;
|
|
16
|
-
const { detectOneToOneRelationships = false, postgrestVersion, defaultSchema = "public", } = opts;
|
|
28
|
+
const { detectOneToOneRelationships = false, postgrestVersion, defaultSchema = "public", format = defaultFormat, } = opts;
|
|
17
29
|
// Ordering of all collections (incl. relationships, columns, args below) is
|
|
18
30
|
// provided by `sortGeneratorMetadata`; this generator no longer re-sorts.
|
|
19
31
|
const introspectionBySchema = Object.fromEntries(schemas.map((s) => [
|
|
@@ -111,6 +123,8 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
111
123
|
view: {
|
|
112
124
|
...materializedView,
|
|
113
125
|
is_updatable: false,
|
|
126
|
+
is_insert_enabled: false,
|
|
127
|
+
is_update_enabled: false,
|
|
114
128
|
},
|
|
115
129
|
relationships: getRelationships(materializedView),
|
|
116
130
|
});
|
|
@@ -339,7 +353,13 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
339
353
|
const getFunctionSignatures = (schema, fns) => {
|
|
340
354
|
return fns
|
|
341
355
|
.map(({ fn, inArgs }) => {
|
|
342
|
-
|
|
356
|
+
// `never` would be wrong here: postgrest-js treats `Args extends { '': Row }`
|
|
357
|
+
// as a computed field (`never extends T` is always true, so same-named table
|
|
358
|
+
// columns get omitted from select results), and an object type with a required
|
|
359
|
+
// `never` property is uninhabited for any tool doing sound type math on the
|
|
360
|
+
// generated types. `Record<PropertyKey, never>` is the accurate type for
|
|
361
|
+
// "callable with no arguments".
|
|
362
|
+
let argsType = "Record<PropertyKey, never>";
|
|
343
363
|
let returnType = getFunctionReturnType(schema, fn);
|
|
344
364
|
// Check for specific error cases
|
|
345
365
|
const conflictError = getConflictError(schema, fns, fn, inArgs);
|
|
@@ -410,6 +430,11 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
410
430
|
}`
|
|
411
431
|
: "";
|
|
412
432
|
function generateNullableUnionTsType(tsType, isNullable) {
|
|
433
|
+
// The generated Json type includes null, so a NOT NULL json/jsonb column
|
|
434
|
+
// has to be narrowed with NonNullable to reflect the database constraint.
|
|
435
|
+
if (tsType === "Json" && !isNullable) {
|
|
436
|
+
return `NonNullable<${tsType}>`;
|
|
437
|
+
}
|
|
413
438
|
// Only add the null union if the type is not unknown as unknown already includes null
|
|
414
439
|
if (tsType === "unknown" || tsType === "any" || !isNullable) {
|
|
415
440
|
return tsType;
|
|
@@ -449,7 +474,8 @@ export type Database = {
|
|
|
449
474
|
}
|
|
450
475
|
Insert: {
|
|
451
476
|
${columnsByTableId[table.id].map((column) => {
|
|
452
|
-
if (column.identity_generation === "ALWAYS"
|
|
477
|
+
if (column.identity_generation === "ALWAYS" ||
|
|
478
|
+
column.is_generated) {
|
|
453
479
|
return `${JSON.stringify(column.name)}?: never`;
|
|
454
480
|
}
|
|
455
481
|
return generateColumnTsDefinition(schema, {
|
|
@@ -465,7 +491,8 @@ export type Database = {
|
|
|
465
491
|
}
|
|
466
492
|
Update: {
|
|
467
493
|
${columnsByTableId[table.id].map((column) => {
|
|
468
|
-
if (column.identity_generation === "ALWAYS"
|
|
494
|
+
if (column.identity_generation === "ALWAYS" ||
|
|
495
|
+
column.is_generated) {
|
|
469
496
|
return `${JSON.stringify(column.name)}?: never`;
|
|
470
497
|
}
|
|
471
498
|
return generateColumnTsDefinition(schema, {
|
|
@@ -500,7 +527,7 @@ export type Database = {
|
|
|
500
527
|
.map(({ fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`),
|
|
501
528
|
]}
|
|
502
529
|
}
|
|
503
|
-
${view.
|
|
530
|
+
${view.is_insert_enabled
|
|
504
531
|
? `Insert: {
|
|
505
532
|
${columnsByTableId[view.id].map((column) => {
|
|
506
533
|
if (!column.is_updatable) {
|
|
@@ -515,7 +542,9 @@ export type Database = {
|
|
|
515
542
|
}, { types, schemas, tables, views });
|
|
516
543
|
})}
|
|
517
544
|
}
|
|
518
|
-
|
|
545
|
+
`
|
|
546
|
+
: ""}${view.is_update_enabled
|
|
547
|
+
? `Update: {
|
|
519
548
|
${columnsByTableId[view.id].map((column) => {
|
|
520
549
|
if (!column.is_updatable) {
|
|
521
550
|
return `${JSON.stringify(column.name)}?: never`;
|
|
@@ -705,10 +734,7 @@ export const Constants = {
|
|
|
705
734
|
})}
|
|
706
735
|
} as const
|
|
707
736
|
`;
|
|
708
|
-
output = await
|
|
709
|
-
parser: "typescript",
|
|
710
|
-
semi: false,
|
|
711
|
-
});
|
|
737
|
+
output = await format(output);
|
|
712
738
|
return output;
|
|
713
739
|
};
|
|
714
740
|
// TODO: Make this more robust. Currently doesn't handle range types - returns them as unknown.
|
|
@@ -32,27 +32,33 @@ export async function introspect(db, opts = {}) {
|
|
|
32
32
|
const systemExcludingFilter = filterByList(included, excluded, DEFAULT_SYSTEM_SCHEMAS);
|
|
33
33
|
const plainFilter = filterByList(included, excluded);
|
|
34
34
|
const queryRows = async (sql) => normalizeRows((await db.query(sql)).rows);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const tables = await
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
// Issued concurrently, matching postgres-meta's CLI path which ran its nine
|
|
36
|
+
// introspection queries under Promise.all. A pooled Queryable runs them in
|
|
37
|
+
// parallel; a single-connection one pipelines them, which is no worse than
|
|
38
|
+
// awaiting each in turn.
|
|
39
|
+
const [schemas, tables, foreignTables, views, materializedViews, columns, primaryKeys, relationships, functions, types,] = await Promise.all([
|
|
40
|
+
queryRows(SCHEMAS_SQL({
|
|
41
|
+
includeSystemSchemas: false,
|
|
42
|
+
nameFilter: systemExcludingFilter,
|
|
43
|
+
})),
|
|
44
|
+
queryRows(TABLES_SQL({ schemaFilter: systemExcludingFilter })),
|
|
45
|
+
queryRows(FOREIGN_TABLES_SQL({ schemaFilter: plainFilter })),
|
|
46
|
+
queryRows(VIEWS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
47
|
+
queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
|
|
48
|
+
queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
49
|
+
queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
50
|
+
listRelationships(db, {
|
|
51
|
+
includedSchemas: included,
|
|
52
|
+
excludedSchemas: excluded,
|
|
53
|
+
}),
|
|
54
|
+
queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
55
|
+
// types: includeSystemSchemas true (no schema filter), table + array types included.
|
|
56
|
+
queryRows(TYPES_SQL({
|
|
57
|
+
schemaFilter: "",
|
|
58
|
+
includeTableTypes: true,
|
|
59
|
+
includeArrayTypes: true,
|
|
60
|
+
})),
|
|
61
|
+
]);
|
|
56
62
|
return {
|
|
57
63
|
version: GENERATOR_METADATA_VERSION,
|
|
58
64
|
schemas: schemas.filter(({ name }) => !excludedSchemas.includes(name) &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { literal } from "pg-format";
|
|
1
|
+
import { literal } from "./pg-format.js";
|
|
2
2
|
export const COLUMNS_SQL = (props) => /* SQL */ `
|
|
3
3
|
-- Adapted from information_schema.columns
|
|
4
4
|
|
|
@@ -43,7 +43,24 @@ SELECT
|
|
|
43
43
|
) AS is_nullable,
|
|
44
44
|
(
|
|
45
45
|
c.relkind IN ('r', 'p')
|
|
46
|
-
|
|
46
|
+
-- Columns of views made writable by INSTEAD OF triggers can be written
|
|
47
|
+
-- through PostgREST even though the view is not auto-updatable, so they
|
|
48
|
+
-- must not degrade to \`?: never\` in generated Insert/Update types.
|
|
49
|
+
-- include_triggers => TRUE covers INSTEAD OF UPDATE triggers, but
|
|
50
|
+
-- pg_column_is_updatable only considers the UPDATE event, so INSTEAD OF
|
|
51
|
+
-- INSERT triggers (tgtype bits: 64 = INSTEAD, 4 = INSERT) are checked
|
|
52
|
+
-- separately. Which events a view accepts is gated per view via
|
|
53
|
+
-- is_insert_enabled / is_update_enabled.
|
|
54
|
+
OR c.relkind IN ('v', 'f') AND (
|
|
55
|
+
pg_column_is_updatable(c.oid, a.attnum, TRUE)
|
|
56
|
+
OR EXISTS (
|
|
57
|
+
SELECT 1 FROM pg_trigger tg
|
|
58
|
+
WHERE tg.tgrelid = c.oid
|
|
59
|
+
AND tg.tgtype & 64 <> 0
|
|
60
|
+
AND tg.tgtype & 4 <> 0
|
|
61
|
+
AND NOT tg.tgisinternal
|
|
62
|
+
)
|
|
63
|
+
)
|
|
47
64
|
) AS is_updatable,
|
|
48
65
|
uniques.table_id IS NOT NULL AS is_unique,
|
|
49
66
|
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.js";
|
|
5
5
|
export const FUNCTIONS_SQL = (props) => /* SQL */ `
|
|
6
6
|
-- CTE with sane arg_modes, arg_names, and arg_types.
|
|
7
7
|
-- All three are always of the same length.
|
|
@@ -6,7 +6,7 @@ export declare const DEFAULT_SYSTEM_SCHEMAS: string[];
|
|
|
6
6
|
/**
|
|
7
7
|
* Build a SQL `IN (...)` / `NOT IN (...)` fragment for an include/exclude list,
|
|
8
8
|
* ported verbatim from `postgres-meta/src/lib/helpers.ts`. Values are escaped
|
|
9
|
-
* with pg-format
|
|
9
|
+
* with the inlined pg-format `literal`. Returns an empty string when there is nothing to
|
|
10
10
|
* filter, so callers interpolate it conditionally.
|
|
11
11
|
*/
|
|
12
12
|
export declare const filterByList: (include?: (string | number)[], exclude?: (string | number)[], defaultExclude?: (string | number)[]) => string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { literal } from "pg-format";
|
|
1
|
+
import { literal } from "./pg-format.js";
|
|
2
2
|
/**
|
|
3
3
|
* Schemas that postgres-meta excludes by default when `includeSystemSchemas`
|
|
4
4
|
* is false. Ported from `postgres-meta/src/lib/constants.ts`.
|
|
@@ -11,7 +11,7 @@ export const DEFAULT_SYSTEM_SCHEMAS = [
|
|
|
11
11
|
/**
|
|
12
12
|
* Build a SQL `IN (...)` / `NOT IN (...)` fragment for an include/exclude list,
|
|
13
13
|
* ported verbatim from `postgres-meta/src/lib/helpers.ts`. Values are escaped
|
|
14
|
-
* with pg-format
|
|
14
|
+
* with the inlined pg-format `literal`. Returns an empty string when there is nothing to
|
|
15
15
|
* filter, so callers interpolate it conditionally.
|
|
16
16
|
*/
|
|
17
17
|
export const filterByList = (include, exclude, defaultExclude) => {
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
* Quote a value as a SQL literal. `null`/`undefined` become `NULL`, booleans
|
|
16
|
+
* `'t'`/`'f'`, dates ISO 8601 strings, arrays comma-separated lists (nested
|
|
17
|
+
* arrays become parenthesised groups), plain objects `'…'::jsonb`, and
|
|
18
|
+
* everything else — including numbers — a single-quoted string with embedded
|
|
19
|
+
* quotes doubled. Strings containing a backslash use the `E'…'` form.
|
|
20
|
+
*/
|
|
21
|
+
export declare function literal(value?: unknown): string;
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
/** Convert a JS ISO string to Postgres' default ISO 8601 form. */
|
|
15
|
+
function formatDate(date) {
|
|
16
|
+
return date.replace("T", " ").replace("Z", "+00");
|
|
17
|
+
}
|
|
18
|
+
function arrayToList(useSpace, array, formatter) {
|
|
19
|
+
let sql = useSpace ? " (" : "(";
|
|
20
|
+
for (const [index, element] of array.entries()) {
|
|
21
|
+
sql += (index === 0 ? "" : ", ") + formatter(element);
|
|
22
|
+
}
|
|
23
|
+
sql += ")";
|
|
24
|
+
return sql;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Quote a value as a SQL literal. `null`/`undefined` become `NULL`, booleans
|
|
28
|
+
* `'t'`/`'f'`, dates ISO 8601 strings, arrays comma-separated lists (nested
|
|
29
|
+
* arrays become parenthesised groups), plain objects `'…'::jsonb`, and
|
|
30
|
+
* everything else — including numbers — a single-quoted string with embedded
|
|
31
|
+
* quotes doubled. Strings containing a backslash use the `E'…'` form.
|
|
32
|
+
*/
|
|
33
|
+
export function literal(value) {
|
|
34
|
+
let text;
|
|
35
|
+
let explicitCast;
|
|
36
|
+
if (value === undefined || value === null) {
|
|
37
|
+
return "NULL";
|
|
38
|
+
}
|
|
39
|
+
if (value === false) {
|
|
40
|
+
return "'f'";
|
|
41
|
+
}
|
|
42
|
+
if (value === true) {
|
|
43
|
+
return "'t'";
|
|
44
|
+
}
|
|
45
|
+
if (value instanceof Date) {
|
|
46
|
+
return `'${formatDate(value.toISOString())}'`;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
for (const [index, element] of value.entries()) {
|
|
51
|
+
if (Array.isArray(element)) {
|
|
52
|
+
parts.push(arrayToList(index !== 0, element, literal));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
parts.push(literal(element));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return parts.toString();
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
text = value;
|
|
62
|
+
}
|
|
63
|
+
else if (typeof value === "number" || typeof value === "bigint") {
|
|
64
|
+
text = value.toString();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
explicitCast = "jsonb";
|
|
68
|
+
text = JSON.stringify(value);
|
|
69
|
+
}
|
|
70
|
+
let hasBackslash = false;
|
|
71
|
+
let quoted = "'";
|
|
72
|
+
for (const c of text) {
|
|
73
|
+
if (c === "'") {
|
|
74
|
+
quoted += c + c;
|
|
75
|
+
}
|
|
76
|
+
else if (c === "\\") {
|
|
77
|
+
quoted += c + c;
|
|
78
|
+
hasBackslash = true;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
quoted += c;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
quoted += "'";
|
|
85
|
+
if (hasBackslash) {
|
|
86
|
+
quoted = `E${quoted}`;
|
|
87
|
+
}
|
|
88
|
+
if (explicitCast) {
|
|
89
|
+
quoted += `::${explicitCast}`;
|
|
90
|
+
}
|
|
91
|
+
return quoted;
|
|
92
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { literal } from "pg-format";
|
|
1
|
+
import { literal } from "./pg-format.js";
|
|
2
2
|
export const VIEWS_SQL = (props) => /* SQL */ `
|
|
3
3
|
SELECT
|
|
4
4
|
c.oid :: int8 AS id,
|
|
@@ -6,6 +6,12 @@ SELECT
|
|
|
6
6
|
c.relname AS name,
|
|
7
7
|
-- See definition of information_schema.views
|
|
8
8
|
(pg_relation_is_updatable(c.oid, false) & 20) = 20 AS is_updatable,
|
|
9
|
+
-- Passing include_triggers => true also counts views made writable by
|
|
10
|
+
-- INSTEAD OF triggers (and INSTEAD rules), which PostgREST can write
|
|
11
|
+
-- through even when the view is not auto-updatable. Bit 8 is INSERT,
|
|
12
|
+
-- bit 4 is UPDATE (see information_schema.views).
|
|
13
|
+
(pg_relation_is_updatable(c.oid, true) & 8) = 8 AS is_insert_enabled,
|
|
14
|
+
(pg_relation_is_updatable(c.oid, true) & 4) = 4 AS is_update_enabled,
|
|
9
15
|
obj_description(c.oid) AS comment
|
|
10
16
|
FROM
|
|
11
17
|
pg_class c
|