@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/README.md CHANGED
@@ -23,6 +23,14 @@ deprecated in favor of this package. It's decided that all four eventually
23
23
  move out to their own SDK repos too (tracked in SDK-1641), not yet scheduled
24
24
  — sequenced after postgres-meta's cutover to this package settles.
25
25
 
26
+ > [!IMPORTANT]
27
+ > **We are no longer accepting contributions that fix the bundled generators.**
28
+ > Because each of the four is moving out to its own language's SDK repository,
29
+ > a fix landed here would have to be made a second time in the destination
30
+ > repository, and it also breaks byte parity with postgres-meta until a release
31
+ > propagates. Please open an issue describing the bug instead, so it can be
32
+ > tracked and carried across the move.
33
+
26
34
  ## Design
27
35
 
28
36
  There is a hard split between **introspection** (database → metadata) and
@@ -128,6 +136,10 @@ Not yet published; consumed in-repo for now (`packages/postgrest-typegen`).
128
136
  npm install pg
129
137
  ```
130
138
 
139
+ ## Releasing
140
+
141
+ 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.
142
+
131
143
  ## License
132
144
 
133
145
  MIT
@@ -0,0 +1,2 @@
1
+ /** Compare two strings with the pinned English collation. */
2
+ export declare function compareStrings(a: string, b: string): number;
@@ -0,0 +1,15 @@
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
+ /** Compare two strings with the pinned English collation. */
13
+ export function compareStrings(a, b) {
14
+ return collator.compare(a, b);
15
+ }
@@ -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)} \`json:"${name}"\``;
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)} \`json:"${name}"\``;
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 {
@@ -1,5 +1,5 @@
1
1
  export { sortGeneratorMetadata } from "../sort.ts";
2
2
  export { generateGo } from "./go.ts";
3
3
  export { generatePython } from "./python.ts";
4
- export { generateTypescript, type GenerateTypescriptOptions, pgTypeToTsType, } from "./typescript.ts";
4
+ export { generateTypescript, type GenerateTypescriptOptions, pgTypeToTsType, type TypeResolutionContext, } from "./typescript.ts";
5
5
  export { type AccessControl, generateSwift, type GenerateSwiftOptions, } from "./swift.ts";
@@ -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, Json
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
- return new PythonBaseModelAttr(attribute.name, type, false);
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.map((item) => `"${item}"`).join(", ");
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="${this.pg_name}")`;
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="${this.pg_name}")]`;
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
- json: "Json[Any]",
281
- jsonb: "Json[Any]",
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",
@@ -1,3 +1,4 @@
1
+ import { compareStrings } from "../collation.js";
1
2
  function formatForSwiftSchemaName(schema) {
2
3
  return `${formatForSwiftTypeName(schema)}Schema`;
3
4
  }
@@ -94,10 +95,52 @@ function pgCompositeTypeToSwiftStruct(type, { types, views, tables, }) {
94
95
  function generateProtocolConformances(protocols) {
95
96
  return protocols.length === 0 ? "" : `: ${protocols.join(", ")}`;
96
97
  }
98
+ /**
99
+ * Serializes a database-provided value as a Swift string literal. Backslashes
100
+ * would otherwise start escape or interpolation sequences (`\(`), double
101
+ * quotes would terminate the literal early, and line breaks or control
102
+ * characters are not allowed in single-line literals, so all of them are
103
+ * escaped per Swift's string literal grammar. U+2028 and U+2029 are legal in
104
+ * Swift literals but are escaped anyway because other tooling mishandles
105
+ * them. Swift unescapes at compile time, so raw values round-trip unchanged.
106
+ */
107
+ function swiftStringLiteral(value) {
108
+ let escaped = "";
109
+ for (const character of value) {
110
+ switch (character) {
111
+ case "\\":
112
+ escaped += "\\\\";
113
+ break;
114
+ case '"':
115
+ escaped += '\\"';
116
+ break;
117
+ case "\t":
118
+ escaped += "\\t";
119
+ break;
120
+ case "\n":
121
+ escaped += "\\n";
122
+ break;
123
+ case "\r":
124
+ escaped += "\\r";
125
+ break;
126
+ default: {
127
+ const codePoint = character.codePointAt(0);
128
+ escaped +=
129
+ codePoint < 0x20 ||
130
+ codePoint === 0x7f ||
131
+ codePoint === 0x2028 ||
132
+ codePoint === 0x2029
133
+ ? `\\u{${codePoint.toString(16)}}`
134
+ : character;
135
+ }
136
+ }
137
+ }
138
+ return `"${escaped}"`;
139
+ }
97
140
  function generateEnum(enum_, { accessControl, level }) {
98
141
  return [
99
142
  `${ident(level)}${accessControl} enum ${enum_.formattedEnumName}${generateProtocolConformances(enum_.protocolConformances)} {`,
100
- ...enum_.cases.map((case_) => `${ident(level + 1)}case ${case_.formattedName} = "${case_.rawValue}"`),
143
+ ...enum_.cases.map((case_) => `${ident(level + 1)}case ${case_.formattedName} = ${swiftStringLiteral(case_.rawValue)}`),
101
144
  `${ident(level)}}`,
102
145
  ];
103
146
  }
@@ -144,10 +187,10 @@ export const generateSwift = (metadata, opts = {}) => {
144
187
  ...schemas.flatMap((schema) => {
145
188
  const schemaTables = [...tables, ...foreignTables]
146
189
  .filter((table) => table.schema === schema.name)
147
- .sort(({ name: a }, { name: b }) => a.localeCompare(b));
190
+ .sort(({ name: a }, { name: b }) => compareStrings(a, b));
148
191
  const schemaViews = [...views, ...materializedViews]
149
192
  .filter((table) => table.schema === schema.name)
150
- .sort(({ name: a }, { name: b }) => a.localeCompare(b));
193
+ .sort(({ name: a }, { name: b }) => compareStrings(a, b));
151
194
  const schemaEnums = types.filter((type) => type.schema === schema.name && type.enums.length > 0);
152
195
  const schemaCompositeTypes = types.filter((type) => type.schema === schema.name && type.attributes.length > 0);
153
196
  return [
@@ -220,6 +263,12 @@ const pgTypeToSwiftType = (pgType, nullable, { types, views, tables, }) => {
220
263
  }
221
264
  else if (["json", "jsonb"].includes(pgType)) {
222
265
  swiftType = "AnyJSON";
266
+ // `void` and `record` are pseudo-types: Postgres rejects them as a column or
267
+ // composite attribute type ("column has pseudo-type void"), and this
268
+ // generator only renders columns and attributes, never functions. So these
269
+ // two arms are unreachable today and deliberately untested. They are kept
270
+ // because they are the mappings to use if Swift ever emits functions, which
271
+ // is where these types do appear.
223
272
  }
224
273
  else if (pgType === "void") {
225
274
  swiftType = "Void";
@@ -1,4 +1,4 @@
1
- import type { GeneratorMetadata, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "../types.ts";
1
+ import type { GeneratorMetadata, PostgresForeignTable, PostgresMaterializedView, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "../types.ts";
2
2
  export interface GenerateTypescriptOptions {
3
3
  /**
4
4
  * Emit `isOneToOne` on each relationship entry. Mirrors postgres-meta's
@@ -24,6 +24,22 @@ export interface GenerateTypescriptOptions {
24
24
  */
25
25
  format?: (code: string) => Promise<string>;
26
26
  }
27
+ /**
28
+ * Collections `pgTypeToTsType` resolves a relation-typed value against.
29
+ *
30
+ * Foreign tables are generated under `Tables` and materialized views under
31
+ * `Views`, exactly like their plain counterparts, so a value typed as one has
32
+ * a `Row` to point at and belongs here. Both are optional so the exported
33
+ * `pgTypeToTsType` signature stays backwards compatible.
34
+ */
35
+ export type TypeResolutionContext = {
36
+ types: PostgresType[];
37
+ schemas: PostgresSchema[];
38
+ tables: PostgresTable[];
39
+ views: PostgresView[];
40
+ foreignTables?: PostgresForeignTable[];
41
+ materializedViews?: PostgresMaterializedView[];
42
+ };
27
43
  /**
28
44
  * Does not re-sort collections; pass input pre-sorted with
29
45
  * `sortGeneratorMetadata` first. Still merge-sorts tables+foreign tables and
@@ -31,9 +47,4 @@ export interface GenerateTypescriptOptions {
31
47
  * can't be expressed by the single-collection `sortGeneratorMetadata` pass.
32
48
  */
33
49
  export declare const generateTypescript: (metadata: GeneratorMetadata, opts?: GenerateTypescriptOptions) => Promise<string>;
34
- export declare const pgTypeToTsType: (schema: PostgresSchema, pgType: string, { types, schemas, tables, views, }: {
35
- types: PostgresType[];
36
- schemas: PostgresSchema[];
37
- tables: PostgresTable[];
38
- views: PostgresView[];
39
- }, typeSchema?: string) => string;
50
+ export declare const pgTypeToTsType: (schema: PostgresSchema, pgType: string, context: TypeResolutionContext, typeSchema?: string) => string;