@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.
Files changed (40) hide show
  1. package/README.md +4 -0
  2. package/dist/generation/go.js +30 -2
  3. package/dist/generation/python.js +24 -10
  4. package/dist/generation/swift.js +43 -1
  5. package/dist/generation/typescript.d.ts +7 -0
  6. package/dist/generation/typescript.js +37 -11
  7. package/dist/introspection/index.js +27 -21
  8. package/dist/introspection/sql/columns.sql.js +19 -2
  9. package/dist/introspection/sql/foreign_tables.sql.js +1 -1
  10. package/dist/introspection/sql/functions.sql.js +1 -1
  11. package/dist/introspection/sql/helpers.d.ts +1 -1
  12. package/dist/introspection/sql/helpers.js +2 -2
  13. package/dist/introspection/sql/materialized_views.sql.js +1 -1
  14. package/dist/introspection/sql/pg-format.d.ts +21 -0
  15. package/dist/introspection/sql/pg-format.js +92 -0
  16. package/dist/introspection/sql/primary_keys.sql.js +1 -1
  17. package/dist/introspection/sql/schemas.sql.js +1 -1
  18. package/dist/introspection/sql/table.sql.js +1 -1
  19. package/dist/introspection/sql/types.sql.js +1 -1
  20. package/dist/introspection/sql/views.sql.js +7 -1
  21. package/dist/types.d.ts +4 -0
  22. package/dist/types.js +9 -0
  23. package/package.json +2 -5
  24. package/src/generation/go.ts +31 -2
  25. package/src/generation/python.ts +25 -10
  26. package/src/generation/swift.ts +44 -1
  27. package/src/generation/typescript.ts +53 -10
  28. package/src/introspection/index.ts +41 -38
  29. package/src/introspection/sql/columns.sql.ts +19 -2
  30. package/src/introspection/sql/foreign_tables.sql.ts +1 -1
  31. package/src/introspection/sql/functions.sql.ts +1 -1
  32. package/src/introspection/sql/helpers.ts +2 -2
  33. package/src/introspection/sql/materialized_views.sql.ts +1 -1
  34. package/src/introspection/sql/pg-format.ts +101 -0
  35. package/src/introspection/sql/primary_keys.sql.ts +1 -1
  36. package/src/introspection/sql/schemas.sql.ts +1 -1
  37. package/src/introspection/sql/table.sql.ts +1 -1
  38. package/src/introspection/sql/types.sql.ts +1 -1
  39. package/src/introspection/sql/views.sql.ts +7 -1
  40. 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
@@ -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 {
@@ -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",
@@ -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} = "${case_.rawValue}"`),
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 prettier from "prettier";
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
- let argsType = "never";
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.is_updatable
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
- Update: {
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 prettier.format(output, {
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
- const schemas = await queryRows(SCHEMAS_SQL({
36
- includeSystemSchemas: false,
37
- nameFilter: systemExcludingFilter,
38
- }));
39
- const tables = await queryRows(TABLES_SQL({ schemaFilter: systemExcludingFilter }));
40
- const foreignTables = await queryRows(FOREIGN_TABLES_SQL({ schemaFilter: plainFilter }));
41
- const views = await queryRows(VIEWS_SQL({ schemaFilter: systemExcludingFilter }));
42
- const materializedViews = await queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter }));
43
- const columns = await queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter }));
44
- const primaryKeys = await queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter }));
45
- const relationships = await listRelationships(db, {
46
- includedSchemas: included,
47
- excludedSchemas: excluded,
48
- });
49
- const functions = await queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter }));
50
- // types: includeSystemSchemas true (no schema filter), table + array types included.
51
- const types = await queryRows(TYPES_SQL({
52
- schemaFilter: "",
53
- includeTableTypes: true,
54
- includeArrayTypes: true,
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
- OR c.relkind IN ('v', 'f') AND pg_column_is_updatable(c.oid, a.attnum, FALSE)
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,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const FOREIGN_TABLES_SQL = (props) => /* SQL */ `
3
3
  SELECT
4
4
  c.oid :: int8 AS id,
@@ -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's `literal`. Returns an empty string when there is nothing to
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's `literal`. Returns an empty string when there is nothing to
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) => {
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const MATERIALIZED_VIEWS_SQL = (props) => /* SQL */ `
3
3
  select
4
4
  c.oid::int8 as id,
@@ -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 PRIMARY_KEYS_SQL = (props) => /* SQL */ `
3
3
  SELECT
4
4
  c.oid :: int8 AS table_id,
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const SCHEMAS_SQL = (props) => /* SQL */ `
3
3
  -- Adapted from information_schema.schemata
4
4
  select
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const TABLES_SQL = (props) => /* SQL */ `
3
3
  SELECT
4
4
  c.oid :: int8 AS id,
@@ -1,4 +1,4 @@
1
- import { literal } from "pg-format";
1
+ import { literal } from "./pg-format.js";
2
2
  export const TYPES_SQL = (props) => /* SQL */ `
3
3
  select
4
4
  t.oid::int8 as id,
@@ -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