@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
@@ -5,6 +5,7 @@
5
5
  // disable comment would corrupt the emitted string.
6
6
  import { format as oxfmtFormat } from "oxfmt";
7
7
  import { VALID_FUNCTION_ARGS_MODE, VALID_UNNAMED_FUNCTION_ARG_TYPES, } from "./constants.js";
8
+ import { compareStrings } from "../collation.js";
8
9
  const defaultFormat = async (code) => {
9
10
  const { code: formatted, errors } = await oxfmtFormat("output.ts", code, {
10
11
  semi: false,
@@ -26,6 +27,16 @@ const defaultFormat = async (code) => {
26
27
  export const generateTypescript = async (metadata, opts = {}) => {
27
28
  const { schemas, tables, foreignTables, views, materializedViews, columns, relationships, functions, types, } = metadata;
28
29
  const { detectOneToOneRelationships = false, postgrestVersion, defaultSchema = "public", format = defaultFormat, } = opts;
30
+ // Every relation-typed value resolves against the same collections, so build
31
+ // the lookup context once rather than reassembling it at each call site.
32
+ const typeContext = {
33
+ types,
34
+ schemas,
35
+ tables,
36
+ views,
37
+ foreignTables,
38
+ materializedViews,
39
+ };
29
40
  // Ordering of all collections (incl. relationships, columns, args below) is
30
41
  // provided by `sortGeneratorMetadata`; this generator no longer re-sorts.
31
42
  const introspectionBySchema = Object.fromEntries(schemas.map((s) => [
@@ -123,6 +134,8 @@ export const generateTypescript = async (metadata, opts = {}) => {
123
134
  view: {
124
135
  ...materializedView,
125
136
  is_updatable: false,
137
+ is_insert_enabled: false,
138
+ is_update_enabled: false,
126
139
  },
127
140
  relationships: getRelationships(materializedView),
128
141
  });
@@ -176,11 +189,11 @@ export const generateTypescript = async (metadata, opts = {}) => {
176
189
  // views — so the cross-collection name sort can't be expressed by the
177
190
  // single-collection `sortGeneratorMetadata` pass and stays here.
178
191
  for (const schema in introspectionBySchema) {
179
- introspectionBySchema[schema].tables.sort((a, b) => a.table.name.localeCompare(b.table.name));
180
- introspectionBySchema[schema].views.sort((a, b) => a.view.name.localeCompare(b.view.name));
181
- introspectionBySchema[schema].functions.sort((a, b) => a.fn.name.localeCompare(b.fn.name));
182
- introspectionBySchema[schema].enums.sort((a, b) => a.name.localeCompare(b.name));
183
- introspectionBySchema[schema].compositeTypes.sort((a, b) => a.name.localeCompare(b.name));
192
+ introspectionBySchema[schema].tables.sort((a, b) => compareStrings(a.table.name, b.table.name));
193
+ introspectionBySchema[schema].views.sort((a, b) => compareStrings(a.view.name, b.view.name));
194
+ introspectionBySchema[schema].functions.sort((a, b) => compareStrings(a.fn.name, b.fn.name));
195
+ introspectionBySchema[schema].enums.sort((a, b) => compareStrings(a.name, b.name));
196
+ introspectionBySchema[schema].compositeTypes.sort((a, b) => compareStrings(a.name, b.name));
184
197
  }
185
198
  const getFunctionTsReturnType = (fn, returnType) => {
186
199
  // Determine if this function should have SetofOptions
@@ -242,12 +255,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
242
255
  const type = typesById.get(type_id);
243
256
  let tsType = "unknown";
244
257
  if (type) {
245
- tsType = pgTypeToTsType(schema, type.name, {
246
- types,
247
- schemas,
248
- tables,
249
- views,
250
- });
258
+ tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
251
259
  }
252
260
  return { name, type: tsType };
253
261
  });
@@ -267,24 +275,14 @@ export const generateTypescript = async (metadata, opts = {}) => {
267
275
  type_schema: column.type_schema,
268
276
  is_nullable: column.is_nullable,
269
277
  is_optional: false,
270
- }, {
271
- types,
272
- schemas,
273
- tables,
274
- views,
275
- }))
278
+ }, typeContext))
276
279
  .join(",\n")}
277
280
  }`;
278
281
  }
279
282
  // Case 3: returns base/array/composite/enum type.
280
283
  const type = typesById.get(fn.return_type_id);
281
284
  if (type) {
282
- return pgTypeToTsType(schema, type.name, {
283
- types,
284
- schemas,
285
- tables,
286
- views,
287
- });
285
+ return pgTypeToTsType(schema, type.name, typeContext, type.schema);
288
286
  }
289
287
  return "unknown";
290
288
  };
@@ -351,7 +349,13 @@ export const generateTypescript = async (metadata, opts = {}) => {
351
349
  const getFunctionSignatures = (schema, fns) => {
352
350
  return fns
353
351
  .map(({ fn, inArgs }) => {
354
- let argsType = "never";
352
+ // `never` would be wrong here: postgrest-js treats `Args extends { '': Row }`
353
+ // as a computed field (`never extends T` is always true, so same-named table
354
+ // columns get omitted from select results), and an object type with a required
355
+ // `never` property is uninhabited for any tool doing sound type math on the
356
+ // generated types. `Record<PropertyKey, never>` is the accurate type for
357
+ // "callable with no arguments".
358
+ let argsType = "Record<PropertyKey, never>";
355
359
  let returnType = getFunctionReturnType(schema, fn);
356
360
  // Check for specific error cases
357
361
  const conflictError = getConflictError(schema, fns, fn, inArgs);
@@ -361,12 +365,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
361
365
  const type = typesById.get(type_id);
362
366
  let tsType = "unknown";
363
367
  if (type) {
364
- tsType = pgTypeToTsType(schema, type.name, {
365
- types,
366
- schemas,
367
- tables,
368
- views,
369
- });
368
+ tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
370
369
  }
371
370
  return { name, type: tsType, has_default };
372
371
  });
@@ -381,12 +380,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
381
380
  const type = typesById.get(type_id);
382
381
  let tsType = "unknown";
383
382
  if (type) {
384
- tsType = pgTypeToTsType(schema, type.name, {
385
- types,
386
- schemas,
387
- tables,
388
- views,
389
- });
383
+ tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
390
384
  }
391
385
  return { name, type: tsType, has_default };
392
386
  });
@@ -399,12 +393,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
399
393
  const type = typesById.get(type_id);
400
394
  let tsType = "unknown";
401
395
  if (type) {
402
- tsType = pgTypeToTsType(schema, type.name, {
403
- types,
404
- schemas,
405
- tables,
406
- views,
407
- });
396
+ tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
408
397
  }
409
398
  return { name, type: tsType, has_default };
410
399
  });
@@ -422,6 +411,11 @@ export const generateTypescript = async (metadata, opts = {}) => {
422
411
  }`
423
412
  : "";
424
413
  function generateNullableUnionTsType(tsType, isNullable) {
414
+ // The generated Json type includes null, so a NOT NULL json/jsonb column
415
+ // has to be narrowed with NonNullable to reflect the database constraint.
416
+ if (tsType === "Json" && !isNullable) {
417
+ return `NonNullable<${tsType}>`;
418
+ }
425
419
  // Only add the null union if the type is not unknown as unknown already includes null
426
420
  if (tsType === "unknown" || tsType === "any" || !isNullable) {
427
421
  return tsType;
@@ -431,6 +425,38 @@ export const generateTypescript = async (metadata, opts = {}) => {
431
425
  function generateColumnTsDefinition(schema, column, context) {
432
426
  return `${JSON.stringify(column.name)}${column.is_optional ? "?" : ""}: ${generateNullableUnionTsType(pgTypeToTsType(schema, column.format, context, column.type_schema), column.is_nullable)}`;
433
427
  }
428
+ /**
429
+ * Is `fn` a PostgREST computed field on `relation` (a table or view)?
430
+ *
431
+ * A computed field is a function taking a single argument of the relation's
432
+ * own composite type, so it is selectable as if it were a column and belongs
433
+ * in `Row`.
434
+ *
435
+ * Matching on the argument's type OID rather than on `argument_types` is what
436
+ * makes named parameters work. `argument_types` is
437
+ * `pg_get_function_arguments()`, which renders the parameter *name* when the
438
+ * parameter has one — `category category` rather than `category` — and
439
+ * schema-qualifies the type when it is not visible in `search_path`. Both
440
+ * forms failed a bare string comparison against the relation name, so
441
+ * `create function name_translated(category category)` silently lost its
442
+ * computed field while the unnamed `name_translated(category)` kept it.
443
+ * See supabase/postgres-meta#1034.
444
+ */
445
+ const isComputedFieldOf = (fn, inArgs, relation) => {
446
+ if (inArgs.length === 1) {
447
+ // `relationTypeByIds` is already "types backed by a relation", so a hit
448
+ // identifies the relation regardless of how the argument was spelled.
449
+ // A `variadic` argument is an array type and never appears here.
450
+ const argType = relationTypeByIds.get(inArgs[0].type_id);
451
+ if (argType) {
452
+ return argType.type_relation_id === relation.id;
453
+ }
454
+ }
455
+ // Fall back to the historical comparison when the argument's type is not in
456
+ // the metadata (callers may pass a reduced `types` list), so a computed
457
+ // field that resolved before keeps resolving.
458
+ return fn.argument_types === relation.name;
459
+ };
434
460
  let output = `
435
461
  export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
436
462
 
@@ -451,9 +477,9 @@ export type Database = {
451
477
  type_schema: column.type_schema,
452
478
  is_nullable: column.is_nullable,
453
479
  is_optional: false,
454
- }, { types, schemas, tables, views })),
480
+ }, typeContext)),
455
481
  ...schemaFunctions
456
- .filter(({ fn }) => fn.argument_types === table.name)
482
+ .filter(({ fn, inArgs }) => isComputedFieldOf(fn, inArgs, table))
457
483
  .map(({ fn }) => {
458
484
  return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`;
459
485
  }),
@@ -461,7 +487,8 @@ export type Database = {
461
487
  }
462
488
  Insert: {
463
489
  ${columnsByTableId[table.id].map((column) => {
464
- if (column.identity_generation === "ALWAYS") {
490
+ if (column.identity_generation === "ALWAYS" ||
491
+ column.is_generated) {
465
492
  return `${JSON.stringify(column.name)}?: never`;
466
493
  }
467
494
  return generateColumnTsDefinition(schema, {
@@ -472,12 +499,13 @@ export type Database = {
472
499
  is_optional: column.is_nullable ||
473
500
  column.is_identity ||
474
501
  column.default_value !== null,
475
- }, { types, schemas, tables, views });
502
+ }, typeContext);
476
503
  })}
477
504
  }
478
505
  Update: {
479
506
  ${columnsByTableId[table.id].map((column) => {
480
- if (column.identity_generation === "ALWAYS") {
507
+ if (column.identity_generation === "ALWAYS" ||
508
+ column.is_generated) {
481
509
  return `${JSON.stringify(column.name)}?: never`;
482
510
  }
483
511
  return generateColumnTsDefinition(schema, {
@@ -486,7 +514,7 @@ export type Database = {
486
514
  type_schema: column.type_schema,
487
515
  is_nullable: column.is_nullable,
488
516
  is_optional: true,
489
- }, { types, schemas, tables, views });
517
+ }, typeContext);
490
518
  })}
491
519
  }
492
520
  Relationships: [
@@ -506,13 +534,16 @@ export type Database = {
506
534
  type_schema: column.type_schema,
507
535
  is_nullable: column.is_nullable,
508
536
  is_optional: false,
509
- }, { types, schemas, tables, views })),
537
+ }, typeContext)),
510
538
  ...schemaFunctions
511
- .filter(({ fn }) => fn.argument_types === view.name)
539
+ .filter(({ fn, inArgs }) => isComputedFieldOf(fn, inArgs, view))
512
540
  .map(({ fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`),
513
541
  ]}
514
542
  }
515
- ${view.is_updatable
543
+ ${
544
+ // Metadata predating the trigger-aware flags (still valid
545
+ // version 1 documents) falls back to the old gate.
546
+ (view.is_insert_enabled ?? view.is_updatable)
516
547
  ? `Insert: {
517
548
  ${columnsByTableId[view.id].map((column) => {
518
549
  if (!column.is_updatable) {
@@ -524,10 +555,12 @@ export type Database = {
524
555
  type_schema: column.type_schema,
525
556
  is_nullable: true,
526
557
  is_optional: true,
527
- }, { types, schemas, tables, views });
558
+ }, typeContext);
528
559
  })}
529
560
  }
530
- Update: {
561
+ `
562
+ : ""}${(view.is_update_enabled ?? view.is_updatable)
563
+ ? `Update: {
531
564
  ${columnsByTableId[view.id].map((column) => {
532
565
  if (!column.is_updatable) {
533
566
  return `${JSON.stringify(column.name)}?: never`;
@@ -538,7 +571,7 @@ export type Database = {
538
571
  type_schema: column.type_schema,
539
572
  is_nullable: true,
540
573
  is_optional: true,
541
- }, { types, schemas, tables, views });
574
+ }, typeContext);
542
575
  })}
543
576
  }
544
577
  `
@@ -558,8 +591,8 @@ export type Database = {
558
591
  return acc;
559
592
  }, {});
560
593
  for (const fnName in schemaFunctionsGroupedByName) {
561
- schemaFunctionsGroupedByName[fnName].sort((a, b) => a.fn.argument_types.localeCompare(b.fn.argument_types) ||
562
- a.fn.return_type.localeCompare(b.fn.return_type));
594
+ schemaFunctionsGroupedByName[fnName].sort((a, b) => compareStrings(a.fn.argument_types, b.fn.argument_types) ||
595
+ compareStrings(a.fn.return_type, b.fn.return_type));
563
596
  }
564
597
  return Object.entries(schemaFunctionsGroupedByName)
565
598
  .map(([fnName, fns]) => {
@@ -584,12 +617,7 @@ export type Database = {
584
617
  const type = typesById.get(type_id);
585
618
  let tsType = "unknown";
586
619
  if (type) {
587
- tsType = `${generateNullableUnionTsType(pgTypeToTsType(schema, type.name, {
588
- types,
589
- schemas,
590
- tables,
591
- views,
592
- }), true)}`;
620
+ tsType = `${generateNullableUnionTsType(pgTypeToTsType(schema, type.name, typeContext, type.schema), true)}`;
593
621
  }
594
622
  return `${JSON.stringify(name)}: ${tsType}`;
595
623
  })}
@@ -721,12 +749,14 @@ export const Constants = {
721
749
  return output;
722
750
  };
723
751
  // TODO: Make this more robust. Currently doesn't handle range types - returns them as unknown.
724
- export const pgTypeToTsType = (schema, pgType, { types, schemas, tables, views, },
725
- // The schema that actually owns `pgType` (a column's `type_schema`), when
726
- // known. Disambiguates same-named enums/composites across schemas; falls
727
- // back to `schema.name` (the table's own schema) when not provided, e.g.
728
- // for function args/return types where no equivalent field exists yet.
752
+ export const pgTypeToTsType = (schema, pgType, context,
753
+ // The schema that actually owns `pgType`, from the column's `type_schema` or
754
+ // the resolved `PostgresType.schema`. Disambiguates same-named enums,
755
+ // composites and relations across schemas. Falls back to `schema.name` (the
756
+ // schema being generated) when not provided, which is only a guess, so pass
757
+ // it whenever the owning schema is known.
729
758
  typeSchema) => {
759
+ const { types, schemas, tables, views, foreignTables = [], materializedViews = [], } = context;
730
760
  if (pgType === "bool") {
731
761
  return "boolean";
732
762
  }
@@ -760,7 +790,7 @@ typeSchema) => {
760
790
  return "Record<string, unknown>";
761
791
  }
762
792
  else if (pgType.startsWith("_")) {
763
- return `(${pgTypeToTsType(schema, pgType.substring(1), { types, schemas, tables, views }, typeSchema)})[]`;
793
+ return `(${pgTypeToTsType(schema, pgType.substring(1), context, typeSchema)})[]`;
764
794
  }
765
795
  else {
766
796
  const preferredSchema = typeSchema ?? schema.name;
@@ -782,21 +812,31 @@ typeSchema) => {
782
812
  }
783
813
  return "unknown";
784
814
  }
785
- const tableRowTypes = tables.filter((table) => table.name === pgType);
786
- if (tableRowTypes.length > 0) {
787
- const tableRowType = tableRowTypes.find((type) => type.schema === preferredSchema) ||
788
- tableRowTypes[0];
789
- if (schemas.some(({ name }) => name === tableRowType.schema)) {
790
- return `Database[${JSON.stringify(tableRowType.schema)}]['Tables'][${JSON.stringify(tableRowType.name)}]['Row']`;
815
+ // A relation-typed value carries only a bare type name, so several relations
816
+ // can share it and `preferredSchema` is what disambiguates them. Match on
817
+ // the schema across every relation kind before falling back to kind order,
818
+ // otherwise a relation in an unrelated schema outranks the one the caller
819
+ // meant. Foreign tables are generated under `Tables` and materialized views
820
+ // under `Views`, alongside their plain counterparts.
821
+ const relationKinds = [
822
+ ["Tables", tables],
823
+ ["Tables", foreignTables],
824
+ ["Views", views],
825
+ ["Views", materializedViews],
826
+ ];
827
+ const findRelation = (matches) => {
828
+ for (const [kind, relations] of relationKinds) {
829
+ const relation = relations.find(matches);
830
+ if (relation)
831
+ return { kind, relation };
791
832
  }
792
- return "unknown";
793
- }
794
- const viewRowTypes = views.filter((view) => view.name === pgType);
795
- if (viewRowTypes.length > 0) {
796
- const viewRowType = viewRowTypes.find((type) => type.schema === preferredSchema) ||
797
- viewRowTypes[0];
798
- if (schemas.some(({ name }) => name === viewRowType.schema)) {
799
- return `Database[${JSON.stringify(viewRowType.schema)}]['Views'][${JSON.stringify(viewRowType.name)}]['Row']`;
833
+ return undefined;
834
+ };
835
+ const match = findRelation(({ name, schema: relationSchema }) => name === pgType && relationSchema === preferredSchema) ?? findRelation(({ name }) => name === pgType);
836
+ if (match) {
837
+ const { kind, relation } = match;
838
+ if (schemas.some(({ name }) => name === relation.schema)) {
839
+ return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(relation.name)}]['Row']`;
800
840
  }
801
841
  return "unknown";
802
842
  }
@@ -22,47 +22,30 @@ import { VIEWS_SQL } from "./sql/views.sql.js";
22
22
  * responsibility — unlike `getGeneratorMetadata`, this does not end the pool.
23
23
  */
24
24
  export async function introspect(db, opts = {}) {
25
- const includedSchemas = opts.includedSchemas ?? [];
26
- const excludedSchemas = opts.excludedSchemas ?? [];
27
- // Managers receive `undefined` (not an empty array) when no filter is set.
28
- const included = includedSchemas.length > 0 ? includedSchemas : undefined;
29
- const excluded = excludedSchemas.length > 0 ? excludedSchemas : undefined;
30
25
  // Most queries exclude the system schemas by default; foreign tables and
31
26
  // materialized views do not (matching the respective manager `list()` calls).
32
- const systemExcludingFilter = filterByList(included, excluded, DEFAULT_SYSTEM_SCHEMAS);
33
- const plainFilter = filterByList(included, excluded);
27
+ const systemExcludingFilter = filterByList(opts.includedSchemas, opts.excludedSchemas, DEFAULT_SYSTEM_SCHEMAS);
28
+ const plainFilter = filterByList(opts.includedSchemas, opts.excludedSchemas);
34
29
  const queryRows = async (sql) => normalizeRows((await db.query(sql)).rows);
35
30
  // Issued concurrently, matching postgres-meta's CLI path which ran its nine
36
31
  // introspection queries under Promise.all. A pooled Queryable runs them in
37
32
  // parallel; a single-connection one pipelines them, which is no worse than
38
33
  // awaiting each in turn.
39
34
  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
- })),
35
+ queryRows(SCHEMAS_SQL({ schemaFilter: systemExcludingFilter })),
44
36
  queryRows(TABLES_SQL({ schemaFilter: systemExcludingFilter })),
45
37
  queryRows(FOREIGN_TABLES_SQL({ schemaFilter: plainFilter })),
46
38
  queryRows(VIEWS_SQL({ schemaFilter: systemExcludingFilter })),
47
39
  queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
48
40
  queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
49
41
  queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
50
- listRelationships(db, {
51
- includedSchemas: included,
52
- excludedSchemas: excluded,
53
- }),
42
+ listRelationships(db, systemExcludingFilter),
54
43
  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
- })),
44
+ queryRows(TYPES_SQL),
61
45
  ]);
62
46
  return {
63
47
  version: GENERATOR_METADATA_VERSION,
64
- schemas: schemas.filter(({ name }) => !excludedSchemas.includes(name) &&
65
- (includedSchemas.length === 0 || includedSchemas.includes(name))),
48
+ schemas,
66
49
  tables,
67
50
  foreignTables,
68
51
  views,
@@ -1,5 +1,5 @@
1
1
  import type { PostgresRelationship } from "../types.ts";
2
- import type { IntrospectOptions, Queryable } from "./index.ts";
2
+ import type { Queryable } from "./index.ts";
3
3
  /**
4
4
  * Port of `postgres-meta/src/lib/PostgresMetaRelationships.ts`.
5
5
  *
@@ -39,9 +39,9 @@ export type ViewKeyDependency = {
39
39
  export declare function expandViewRelationships(allTableM2oAndO2oRelationships: PostgresRelationship[], viewsKeyDependencies: ViewKeyDependency[]): PostgresRelationship[];
40
40
  /**
41
41
  * List all relationships (table↔table plus the view-derived ones) for the
42
- * given schema filter. Mirrors `PostgresMetaRelationships.list()` with the
43
- * generator path's defaults (`includeSystemSchemas: false`). Errors from the
42
+ * schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
43
+ * `introspect()` builds with the system schemas excluded. Errors from the
44
44
  * injected `Queryable` propagate by throwing — callers adapt.
45
45
  */
46
- export declare function listRelationships(db: Queryable, { includedSchemas, excludedSchemas }?: IntrospectOptions): Promise<PostgresRelationship[]>;
46
+ export declare function listRelationships(db: Queryable, schemaFilter: string): Promise<PostgresRelationship[]>;
47
47
  export {};
@@ -1,4 +1,3 @@
1
- import { DEFAULT_SYSTEM_SCHEMAS, filterByList } from "./sql/helpers.js";
2
1
  import { TABLE_RELATIONSHIPS_SQL } from "./sql/table_relationships.sql.js";
3
2
  import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.js";
4
3
  /**
@@ -13,12 +12,7 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
13
12
  return allTableM2oAndO2oRelationships.flatMap((r) => {
14
13
  const expandKeyDepCols = (colDeps) => {
15
14
  const tableColumns = colDeps.map(({ table_column }) => table_column);
16
- // https://gist.github.com/ssippe/1f92625532eef28be6974f898efb23ef?permalink_comment_id=3474581#gistcomment-3474581
17
- const cartesianProduct = (allEntries) => {
18
- return allEntries.reduce((results, entries) => results
19
- .map((result) => entries.map((entry) => result.concat(entry)))
20
- .reduce((subResults, result) => subResults.concat(result), []), [[]]);
21
- };
15
+ const cartesianProduct = (lists) => lists.reduce((combinations, entries) => combinations.flatMap((combination) => entries.map((entry) => [...combination, entry])), [[]]);
22
16
  const viewColumnsPermutations = cartesianProduct(colDeps.map((cd) => cd.view_columns));
23
17
  return viewColumnsPermutations.map((viewColumns) => ({
24
18
  tableColumns,
@@ -72,12 +66,11 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
72
66
  }
73
67
  /**
74
68
  * List all relationships (table↔table plus the view-derived ones) for the
75
- * given schema filter. Mirrors `PostgresMetaRelationships.list()` with the
76
- * generator path's defaults (`includeSystemSchemas: false`). Errors from the
69
+ * schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
70
+ * `introspect()` builds with the system schemas excluded. Errors from the
77
71
  * injected `Queryable` propagate by throwing — callers adapt.
78
72
  */
79
- export async function listRelationships(db, { includedSchemas, excludedSchemas } = {}) {
80
- const schemaFilter = filterByList(includedSchemas, excludedSchemas, DEFAULT_SYSTEM_SCHEMAS);
73
+ export async function listRelationships(db, schemaFilter) {
81
74
  const { rows: allTableM2oAndO2oRelationships } = await db.query(TABLE_RELATIONSHIPS_SQL({ schemaFilter }));
82
75
  const { rows: viewsKeyDependencies } = await db.query(VIEWS_KEY_DEPENDENCIES_SQL({ schemaFilter }));
83
76
  const allViewM2oAndO2oRelationships = expandViewRelationships(allTableM2oAndO2oRelationships, viewsKeyDependencies);
@@ -1,7 +1,2 @@
1
- import type { SQLQueryPropsWithSchemaFilter } from "./common.ts";
2
- export declare const COLUMNS_SQL: (props: SQLQueryPropsWithSchemaFilter & {
3
- tableIdFilter?: string;
4
- tableIdentifierFilter?: string;
5
- columnNameFilter?: string;
6
- idsFilter?: string;
7
- }) => string;
1
+ import type { SchemaFilterProps } from "./common.ts";
2
+ export declare const COLUMNS_SQL: (props: SchemaFilterProps) => string;
@@ -1,4 +1,3 @@
1
- import { literal } from "pg-format";
2
1
  export const COLUMNS_SQL = (props) => /* SQL */ `
3
2
  -- Adapted from information_schema.columns
4
3
 
@@ -36,14 +35,44 @@ SELECT
36
35
  WHEN 'd' THEN 'BY DEFAULT'
37
36
  ELSE NULL
38
37
  END AS identity_generation,
39
- a.attgenerated IN ('s') AS is_generated,
38
+ -- 's' = stored, 'v' = virtual (PostgreSQL 18+); neither accepts writes.
39
+ a.attgenerated IN ('s', 'v') AS is_generated,
40
40
  NOT (
41
41
  a.attnotnull
42
42
  OR t.typtype = 'd' AND t.typnotnull
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 or unconditional
47
+ -- INSTEAD rules can be written through PostgREST even though the view is
48
+ -- not auto-updatable, so they must not degrade to \`?: never\` in generated
49
+ -- Insert/Update types. pg_column_is_updatable cannot express this: it
50
+ -- requires the relation to support both UPDATE and DELETE, so a view with
51
+ -- only an INSTEAD OF INSERT or only an INSTEAD OF UPDATE trigger reports
52
+ -- every column as non-updatable regardless of include_triggers. Triggers
53
+ -- and rules rewrite whole rows, so their presence makes every column
54
+ -- writable (tgtype bits: 64 = INSTEAD, 4 = INSERT, 16 = UPDATE;
55
+ -- pg_rewrite ev_type: '2' = UPDATE, '3' = INSERT; ev_qual is '<>' for
56
+ -- unconditional rules, and only unconditional INSTEAD rules make a view
57
+ -- writable). Which events a view accepts is gated per view via
58
+ -- is_insert_enabled / is_update_enabled.
59
+ OR c.relkind IN ('v', 'f') AND (
60
+ pg_column_is_updatable(c.oid, a.attnum, FALSE)
61
+ OR EXISTS (
62
+ SELECT 1 FROM pg_trigger tg
63
+ WHERE tg.tgrelid = c.oid
64
+ AND tg.tgtype & 64 <> 0
65
+ AND tg.tgtype & 20 <> 0
66
+ AND NOT tg.tgisinternal
67
+ )
68
+ OR EXISTS (
69
+ SELECT 1 FROM pg_rewrite rw
70
+ WHERE rw.ev_class = c.oid
71
+ AND rw.is_instead
72
+ AND rw.ev_type IN ('2', '3')
73
+ AND rw.ev_qual :: text = '<>'
74
+ )
75
+ )
47
76
  ) AS is_updatable,
48
77
  uniques.table_id IS NOT NULL AS is_unique,
49
78
  check_constraints.definition AS "check",
@@ -101,10 +130,6 @@ FROM
101
130
  ) AS check_constraints ON check_constraints.table_id = c.oid AND check_constraints.ordinal_position = a.attnum
102
131
  WHERE
103
132
  ${props.schemaFilter ? `nc.nspname ${props.schemaFilter} AND` : ""}
104
- ${props.idsFilter ? `(c.oid || '.' || a.attnum) ${props.idsFilter} AND` : ""}
105
- ${props.columnNameFilter ? `(c.relname || '.' || a.attname) ${props.columnNameFilter} AND` : ""}
106
- ${props.tableIdFilter ? `c.oid ${props.tableIdFilter} AND` : ""}
107
- ${props.tableIdentifierFilter ? `nc.nspname || '.' || c.relname ${props.tableIdentifierFilter} AND` : ""}
108
133
  NOT pg_is_other_temp_schema(nc.oid)
109
134
  AND a.attnum > 0
110
135
  AND NOT a.attisdropped
@@ -117,6 +142,4 @@ WHERE
117
142
  'SELECT, INSERT, UPDATE, REFERENCES'
118
143
  )
119
144
  )
120
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
121
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
122
145
  `;
@@ -1,16 +1,8 @@
1
- /**
2
- * Shared prop shapes for the SQL builders, ported from
3
- * `postgres-meta/src/lib/sql/common.ts`. Only the variants used by the
4
- * typegen introspection path are kept.
5
- */
6
- export type SQLQueryProps = {
7
- limit?: number;
8
- offset?: number;
9
- };
10
- export type SQLQueryPropsWithSchemaFilter = SQLQueryProps & {
11
- schemaFilter?: string;
12
- };
13
- export type SQLQueryPropsWithSchemaFilterAndIdsFilter = SQLQueryProps & {
14
- schemaFilter?: string;
15
- idsFilter?: string;
1
+ /** The only option the introspection passes to its SQL builders. */
2
+ export type SchemaFilterProps = {
3
+ /**
4
+ * An `IN (…)` or `NOT IN (…)` fragment from `filterByList`, or the empty
5
+ * string for no filter.
6
+ */
7
+ schemaFilter: string;
16
8
  };
@@ -1,6 +1,2 @@
1
- import type { SQLQueryProps } from "./common.ts";
2
- export declare const FOREIGN_TABLES_SQL: (props: SQLQueryProps & {
3
- schemaFilter?: string;
4
- idsFilter?: string;
5
- tableIdentifierFilter?: string;
6
- }) => string;
1
+ import type { SchemaFilterProps } from "./common.ts";
2
+ export declare const FOREIGN_TABLES_SQL: (props: SchemaFilterProps) => string;
@@ -1,4 +1,3 @@
1
- import { literal } from "pg-format";
2
1
  export const FOREIGN_TABLES_SQL = (props) => /* SQL */ `
3
2
  SELECT
4
3
  c.oid :: int8 AS id,
@@ -10,9 +9,5 @@ FROM
10
9
  JOIN pg_namespace n ON n.oid = c.relnamespace
11
10
  WHERE
12
11
  ${props.schemaFilter ? `n.nspname ${props.schemaFilter} AND` : ""}
13
- ${props.idsFilter ? `c.oid ${props.idsFilter} AND` : ""}
14
- ${props.tableIdentifierFilter ? `(n.nspname || '.' || c.relname) ${props.tableIdentifierFilter} AND` : ""}
15
12
  c.relkind = 'f'
16
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
17
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
18
13
  `;
@@ -1,5 +1,2 @@
1
- import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
2
- export declare const FUNCTIONS_SQL: (props: SQLQueryPropsWithSchemaFilterAndIdsFilter & {
3
- nameFilter?: string;
4
- args?: string[];
5
- }) => string;
1
+ import type { SchemaFilterProps } from "./common.ts";
2
+ export declare const FUNCTIONS_SQL: (props: SchemaFilterProps) => string;