@supabase/postgrest-typegen 0.2.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.js +22 -5
- 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 -4
- 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 +28 -5
- 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
|
}
|
|
@@ -123,6 +123,8 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
123
123
|
view: {
|
|
124
124
|
...materializedView,
|
|
125
125
|
is_updatable: false,
|
|
126
|
+
is_insert_enabled: false,
|
|
127
|
+
is_update_enabled: false,
|
|
126
128
|
},
|
|
127
129
|
relationships: getRelationships(materializedView),
|
|
128
130
|
});
|
|
@@ -351,7 +353,13 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
351
353
|
const getFunctionSignatures = (schema, fns) => {
|
|
352
354
|
return fns
|
|
353
355
|
.map(({ fn, inArgs }) => {
|
|
354
|
-
|
|
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>";
|
|
355
363
|
let returnType = getFunctionReturnType(schema, fn);
|
|
356
364
|
// Check for specific error cases
|
|
357
365
|
const conflictError = getConflictError(schema, fns, fn, inArgs);
|
|
@@ -422,6 +430,11 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
422
430
|
}`
|
|
423
431
|
: "";
|
|
424
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
|
+
}
|
|
425
438
|
// Only add the null union if the type is not unknown as unknown already includes null
|
|
426
439
|
if (tsType === "unknown" || tsType === "any" || !isNullable) {
|
|
427
440
|
return tsType;
|
|
@@ -461,7 +474,8 @@ export type Database = {
|
|
|
461
474
|
}
|
|
462
475
|
Insert: {
|
|
463
476
|
${columnsByTableId[table.id].map((column) => {
|
|
464
|
-
if (column.identity_generation === "ALWAYS"
|
|
477
|
+
if (column.identity_generation === "ALWAYS" ||
|
|
478
|
+
column.is_generated) {
|
|
465
479
|
return `${JSON.stringify(column.name)}?: never`;
|
|
466
480
|
}
|
|
467
481
|
return generateColumnTsDefinition(schema, {
|
|
@@ -477,7 +491,8 @@ export type Database = {
|
|
|
477
491
|
}
|
|
478
492
|
Update: {
|
|
479
493
|
${columnsByTableId[table.id].map((column) => {
|
|
480
|
-
if (column.identity_generation === "ALWAYS"
|
|
494
|
+
if (column.identity_generation === "ALWAYS" ||
|
|
495
|
+
column.is_generated) {
|
|
481
496
|
return `${JSON.stringify(column.name)}?: never`;
|
|
482
497
|
}
|
|
483
498
|
return generateColumnTsDefinition(schema, {
|
|
@@ -512,7 +527,7 @@ export type Database = {
|
|
|
512
527
|
.map(({ fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`),
|
|
513
528
|
]}
|
|
514
529
|
}
|
|
515
|
-
${view.
|
|
530
|
+
${view.is_insert_enabled
|
|
516
531
|
? `Insert: {
|
|
517
532
|
${columnsByTableId[view.id].map((column) => {
|
|
518
533
|
if (!column.is_updatable) {
|
|
@@ -527,7 +542,9 @@ export type Database = {
|
|
|
527
542
|
}, { types, schemas, tables, views });
|
|
528
543
|
})}
|
|
529
544
|
}
|
|
530
|
-
|
|
545
|
+
`
|
|
546
|
+
: ""}${view.is_update_enabled
|
|
547
|
+
? `Update: {
|
|
531
548
|
${columnsByTableId[view.id].map((column) => {
|
|
532
549
|
if (!column.is_updatable) {
|
|
533
550
|
return `${JSON.stringify(column.name)}?: never`;
|
|
@@ -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
|
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.2.
|
|
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
|
-
"oxfmt": "0.65.0"
|
|
66
|
-
"pg-format": "1.0.4"
|
|
65
|
+
"oxfmt": "0.65.0"
|
|
67
66
|
},
|
|
68
67
|
"devDependencies": {
|
|
69
68
|
"@testcontainers/postgresql": "^12.1.0",
|
|
@@ -72,7 +71,6 @@
|
|
|
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
75
|
"oxlint": "1.80.0",
|
|
78
76
|
"oxlint-tsgolint": "7.0.2001",
|
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
|
];
|
|
@@ -232,6 +232,8 @@ export const generateTypescript = async (
|
|
|
232
232
|
view: {
|
|
233
233
|
...materializedView,
|
|
234
234
|
is_updatable: false,
|
|
235
|
+
is_insert_enabled: false,
|
|
236
|
+
is_update_enabled: false,
|
|
235
237
|
},
|
|
236
238
|
relationships: getRelationships(materializedView),
|
|
237
239
|
});
|
|
@@ -547,7 +549,13 @@ export const generateTypescript = async (
|
|
|
547
549
|
) => {
|
|
548
550
|
return fns
|
|
549
551
|
.map(({ fn, inArgs }) => {
|
|
550
|
-
|
|
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>";
|
|
551
559
|
let returnType = getFunctionReturnType(schema, fn);
|
|
552
560
|
|
|
553
561
|
// Check for specific error cases
|
|
@@ -637,6 +645,11 @@ export const generateTypescript = async (
|
|
|
637
645
|
: "";
|
|
638
646
|
|
|
639
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
|
+
}
|
|
640
653
|
// Only add the null union if the type is not unknown as unknown already includes null
|
|
641
654
|
if (tsType === "unknown" || tsType === "any" || !isNullable) {
|
|
642
655
|
return tsType;
|
|
@@ -716,7 +729,10 @@ export type Database = {
|
|
|
716
729
|
}
|
|
717
730
|
Insert: {
|
|
718
731
|
${columnsByTableId[table.id].map((column) => {
|
|
719
|
-
if (
|
|
732
|
+
if (
|
|
733
|
+
column.identity_generation === "ALWAYS" ||
|
|
734
|
+
column.is_generated
|
|
735
|
+
) {
|
|
720
736
|
return `${JSON.stringify(column.name)}?: never`;
|
|
721
737
|
}
|
|
722
738
|
return generateColumnTsDefinition(
|
|
@@ -737,7 +753,10 @@ export type Database = {
|
|
|
737
753
|
}
|
|
738
754
|
Update: {
|
|
739
755
|
${columnsByTableId[table.id].map((column) => {
|
|
740
|
-
if (
|
|
756
|
+
if (
|
|
757
|
+
column.identity_generation === "ALWAYS" ||
|
|
758
|
+
column.is_generated
|
|
759
|
+
) {
|
|
741
760
|
return `${JSON.stringify(column.name)}?: never`;
|
|
742
761
|
}
|
|
743
762
|
|
|
@@ -797,7 +816,7 @@ export type Database = {
|
|
|
797
816
|
]}
|
|
798
817
|
}
|
|
799
818
|
${
|
|
800
|
-
view.
|
|
819
|
+
view.is_insert_enabled
|
|
801
820
|
? `Insert: {
|
|
802
821
|
${columnsByTableId[view.id].map((column) => {
|
|
803
822
|
if (!column.is_updatable) {
|
|
@@ -816,7 +835,11 @@ export type Database = {
|
|
|
816
835
|
);
|
|
817
836
|
})}
|
|
818
837
|
}
|
|
819
|
-
|
|
838
|
+
`
|
|
839
|
+
: ""
|
|
840
|
+
}${
|
|
841
|
+
view.is_update_enabled
|
|
842
|
+
? `Update: {
|
|
820
843
|
${columnsByTableId[view.id].map((column) => {
|
|
821
844
|
if (!column.is_updatable) {
|
|
822
845
|
return `${JSON.stringify(column.name)}?: never`;
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { literal } from "pg-format";
|
|
1
|
+
import { literal } from "./pg-format.ts";
|
|
2
2
|
import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
|
|
3
3
|
|
|
4
4
|
export const VIEWS_SQL = (
|
|
@@ -12,6 +12,12 @@ SELECT
|
|
|
12
12
|
c.relname AS name,
|
|
13
13
|
-- See definition of information_schema.views
|
|
14
14
|
(pg_relation_is_updatable(c.oid, false) & 20) = 20 AS is_updatable,
|
|
15
|
+
-- Passing include_triggers => true also counts views made writable by
|
|
16
|
+
-- INSTEAD OF triggers (and INSTEAD rules), which PostgREST can write
|
|
17
|
+
-- through even when the view is not auto-updatable. Bit 8 is INSERT,
|
|
18
|
+
-- bit 4 is UPDATE (see information_schema.views).
|
|
19
|
+
(pg_relation_is_updatable(c.oid, true) & 8) = 8 AS is_insert_enabled,
|
|
20
|
+
(pg_relation_is_updatable(c.oid, true) & 4) = 4 AS is_update_enabled,
|
|
15
21
|
obj_description(c.oid) AS comment
|
|
16
22
|
FROM
|
|
17
23
|
pg_class c
|
package/src/types.ts
CHANGED
|
@@ -156,6 +156,15 @@ const postgresViewSchema = type({
|
|
|
156
156
|
schema: "string",
|
|
157
157
|
name: "string",
|
|
158
158
|
is_updatable: "boolean",
|
|
159
|
+
/**
|
|
160
|
+
* Whether INSERT works through this view: auto-updatable per
|
|
161
|
+
* `pg_relation_is_updatable`, or made insertable by an INSTEAD OF INSERT
|
|
162
|
+
* trigger. Tracked separately from `is_updatable` (which mirrors
|
|
163
|
+
* `information_schema.views` and ignores triggers).
|
|
164
|
+
*/
|
|
165
|
+
is_insert_enabled: "boolean",
|
|
166
|
+
/** Same as `is_insert_enabled`, for UPDATE. */
|
|
167
|
+
is_update_enabled: "boolean",
|
|
159
168
|
comment: "string | null",
|
|
160
169
|
"columns?": postgresColumnSchema.array(),
|
|
161
170
|
});
|