@supabase/postgrest-typegen 0.2.0 → 0.2.2

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.
Files changed (66) hide show
  1. package/README.md +12 -0
  2. package/dist/collation.d.ts +2 -0
  3. package/dist/collation.js +15 -0
  4. package/dist/generation/go.js +30 -2
  5. package/dist/generation/index.d.ts +1 -1
  6. package/dist/generation/python.js +24 -10
  7. package/dist/generation/swift.js +52 -3
  8. package/dist/generation/typescript.d.ts +18 -7
  9. package/dist/generation/typescript.js +122 -82
  10. package/dist/introspection/index.js +6 -23
  11. package/dist/introspection/relationships.d.ts +4 -4
  12. package/dist/introspection/relationships.js +4 -11
  13. package/dist/introspection/sql/columns.sql.d.ts +2 -7
  14. package/dist/introspection/sql/columns.sql.js +32 -9
  15. package/dist/introspection/sql/common.d.ts +7 -15
  16. package/dist/introspection/sql/foreign_tables.sql.d.ts +2 -6
  17. package/dist/introspection/sql/foreign_tables.sql.js +0 -5
  18. package/dist/introspection/sql/functions.sql.d.ts +2 -5
  19. package/dist/introspection/sql/functions.sql.js +0 -33
  20. package/dist/introspection/sql/helpers.d.ts +1 -1
  21. package/dist/introspection/sql/helpers.js +2 -2
  22. package/dist/introspection/sql/materialized_views.sql.d.ts +2 -4
  23. package/dist/introspection/sql/materialized_views.sql.js +0 -5
  24. package/dist/introspection/sql/pg-format.d.ts +21 -0
  25. package/dist/introspection/sql/pg-format.js +92 -0
  26. package/dist/introspection/sql/primary_keys.sql.d.ts +2 -4
  27. package/dist/introspection/sql/primary_keys.sql.js +0 -4
  28. package/dist/introspection/sql/schemas.sql.d.ts +2 -6
  29. package/dist/introspection/sql/schemas.sql.js +2 -6
  30. package/dist/introspection/sql/table.sql.d.ts +2 -4
  31. package/dist/introspection/sql/table.sql.js +0 -5
  32. package/dist/introspection/sql/table_relationships.sql.d.ts +2 -2
  33. package/dist/introspection/sql/types.sql.d.ts +5 -5
  34. package/dist/introspection/sql/types.sql.js +6 -17
  35. package/dist/introspection/sql/views.sql.d.ts +2 -4
  36. package/dist/introspection/sql/views.sql.js +6 -5
  37. package/dist/introspection/sql/views_key_dependencies.sql.d.ts +2 -2
  38. package/dist/introspection/sql/views_key_dependencies.sql.js +64 -75
  39. package/dist/sort.js +16 -15
  40. package/dist/types.d.ts +4 -0
  41. package/dist/types.js +11 -0
  42. package/package.json +3 -5
  43. package/src/collation.ts +16 -0
  44. package/src/generation/go.ts +31 -2
  45. package/src/generation/index.ts +1 -0
  46. package/src/generation/python.ts +25 -10
  47. package/src/generation/swift.ts +53 -3
  48. package/src/generation/typescript.ts +187 -107
  49. package/src/introspection/index.ts +7 -32
  50. package/src/introspection/relationships.ts +10 -19
  51. package/src/introspection/sql/columns.sql.ts +34 -18
  52. package/src/introspection/sql/common.ts +7 -17
  53. package/src/introspection/sql/foreign_tables.sql.ts +2 -13
  54. package/src/introspection/sql/functions.sql.ts +2 -44
  55. package/src/introspection/sql/helpers.ts +2 -2
  56. package/src/introspection/sql/materialized_views.sql.ts +2 -11
  57. package/src/introspection/sql/pg-format.ts +101 -0
  58. package/src/introspection/sql/primary_keys.sql.ts +2 -10
  59. package/src/introspection/sql/schemas.sql.ts +4 -14
  60. package/src/introspection/sql/table.sql.ts +2 -11
  61. package/src/introspection/sql/table_relationships.sql.ts +2 -4
  62. package/src/introspection/sql/types.sql.ts +6 -26
  63. package/src/introspection/sql/views.sql.ts +8 -11
  64. package/src/introspection/sql/views_key_dependencies.sql.ts +83 -77
  65. package/src/sort.ts +20 -15
  66. package/src/types.ts +11 -0
package/dist/types.js CHANGED
@@ -136,6 +136,17 @@ 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 or an unconditional INSTEAD rule. Tracked separately from
143
+ * `is_updatable` (which mirrors `information_schema.views` and ignores
144
+ * triggers). Optional because version 1 metadata predating this field is
145
+ * still valid; consumers fall back to `is_updatable` when absent.
146
+ */
147
+ "is_insert_enabled?": "boolean",
148
+ /** Same as `is_insert_enabled`, for UPDATE. */
149
+ "is_update_enabled?": "boolean",
139
150
  comment: "string | null",
140
151
  "columns?": postgresColumnSchema.array(),
141
152
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/postgrest-typegen",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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.66.0"
67
66
  },
68
67
  "devDependencies": {
69
68
  "@testcontainers/postgresql": "^12.1.0",
@@ -72,9 +71,8 @@
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
- "oxlint": "1.80.0",
75
+ "oxlint": "1.81.0",
78
76
  "oxlint-tsgolint": "7.0.2001",
79
77
  "pg": "^8.23.0",
80
78
  "testcontainers": "^12.1.0",
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The one string comparison every ordering in this package uses.
3
+ *
4
+ * Generated output is ordered by name, and `String.prototype.localeCompare`
5
+ * without a locale follows the host's default locale, so the same database
6
+ * produced differently ordered code on differently configured machines (a
7
+ * Swedish host sorts `ä` after `z`, an English one next to `a`). Pinning the
8
+ * collator to English keeps the order postgres-meta's output has on the
9
+ * English CI runners and makes it the same everywhere else.
10
+ */
11
+ const collator = new Intl.Collator("en");
12
+
13
+ /** Compare two strings with the pinned English collation. */
14
+ export function compareStrings(a: string, b: string): number {
15
+ return collator.compare(a, b);
16
+ }
@@ -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
- )} \`json:"${name}"\``;
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
- )} \`json:"${name}"\``;
264
+ )} ${formatForGoStructTag(name)}`;
236
265
  },
237
266
  );
238
267
 
@@ -5,6 +5,7 @@ export {
5
5
  generateTypescript,
6
6
  type GenerateTypescriptOptions,
7
7
  pgTypeToTsType,
8
+ type TypeResolutionContext,
8
9
  } from "./typescript.ts";
9
10
  export {
10
11
  type AccessControl,
@@ -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, Json
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
- return new PythonBaseModelAttr(attribute.name, type, false);
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.map((item) => `"${item}"`).join(", ");
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="${this.pg_name}")`;
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="${this.pg_name}")]`;
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
- json: "Json[Any]",
400
- jsonb: "Json[Any]",
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",
@@ -7,6 +7,7 @@ import type {
7
7
  PostgresType,
8
8
  PostgresView,
9
9
  } from "../types.ts";
10
+ import { compareStrings } from "../collation.ts";
10
11
 
11
12
  type Operation = "Select" | "Insert" | "Update";
12
13
  export type AccessControl = "internal" | "public" | "private" | "package";
@@ -174,6 +175,49 @@ function generateProtocolConformances(protocols: string[]): string {
174
175
  return protocols.length === 0 ? "" : `: ${protocols.join(", ")}`;
175
176
  }
176
177
 
178
+ /**
179
+ * Serializes a database-provided value as a Swift string literal. Backslashes
180
+ * would otherwise start escape or interpolation sequences (`\(`), double
181
+ * quotes would terminate the literal early, and line breaks or control
182
+ * characters are not allowed in single-line literals, so all of them are
183
+ * escaped per Swift's string literal grammar. U+2028 and U+2029 are legal in
184
+ * Swift literals but are escaped anyway because other tooling mishandles
185
+ * them. Swift unescapes at compile time, so raw values round-trip unchanged.
186
+ */
187
+ function swiftStringLiteral(value: string): string {
188
+ let escaped = "";
189
+ for (const character of value) {
190
+ switch (character) {
191
+ case "\\":
192
+ escaped += "\\\\";
193
+ break;
194
+ case '"':
195
+ escaped += '\\"';
196
+ break;
197
+ case "\t":
198
+ escaped += "\\t";
199
+ break;
200
+ case "\n":
201
+ escaped += "\\n";
202
+ break;
203
+ case "\r":
204
+ escaped += "\\r";
205
+ break;
206
+ default: {
207
+ const codePoint = character.codePointAt(0)!;
208
+ escaped +=
209
+ codePoint < 0x20 ||
210
+ codePoint === 0x7f ||
211
+ codePoint === 0x2028 ||
212
+ codePoint === 0x2029
213
+ ? `\\u{${codePoint.toString(16)}}`
214
+ : character;
215
+ }
216
+ }
217
+ }
218
+ return `"${escaped}"`;
219
+ }
220
+
177
221
  function generateEnum(
178
222
  enum_: SwiftEnum,
179
223
  { accessControl, level }: SwiftGeneratorOptions & { level: number },
@@ -184,7 +228,7 @@ function generateEnum(
184
228
  )} {`,
185
229
  ...enum_.cases.map(
186
230
  (case_) =>
187
- `${ident(level + 1)}case ${case_.formattedName} = "${case_.rawValue}"`,
231
+ `${ident(level + 1)}case ${case_.formattedName} = ${swiftStringLiteral(case_.rawValue)}`,
188
232
  ),
189
233
  `${ident(level)}}`,
190
234
  ];
@@ -270,11 +314,11 @@ export const generateSwift = (
270
314
  ...schemas.flatMap((schema) => {
271
315
  const schemaTables = [...tables, ...foreignTables]
272
316
  .filter((table) => table.schema === schema.name)
273
- .sort(({ name: a }, { name: b }) => a.localeCompare(b));
317
+ .sort(({ name: a }, { name: b }) => compareStrings(a, b));
274
318
 
275
319
  const schemaViews = [...views, ...materializedViews]
276
320
  .filter((table) => table.schema === schema.name)
277
- .sort(({ name: a }, { name: b }) => a.localeCompare(b));
321
+ .sort(({ name: a }, { name: b }) => compareStrings(a, b));
278
322
 
279
323
  const schemaEnums = types.filter(
280
324
  (type) => type.schema === schema.name && type.enums.length > 0,
@@ -381,6 +425,12 @@ const pgTypeToSwiftType = (
381
425
  swiftType = "String";
382
426
  } else if (["json", "jsonb"].includes(pgType)) {
383
427
  swiftType = "AnyJSON";
428
+ // `void` and `record` are pseudo-types: Postgres rejects them as a column or
429
+ // composite attribute type ("column has pseudo-type void"), and this
430
+ // generator only renders columns and attributes, never functions. So these
431
+ // two arms are unreachable today and deliberately untested. They are kept
432
+ // because they are the mappings to use if Swift ever emits functions, which
433
+ // is where these types do appear.
384
434
  } else if (pgType === "void") {
385
435
  swiftType = "Void";
386
436
  } else if (pgType === "record") {