@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.
- package/README.md +12 -0
- package/dist/collation.d.ts +2 -0
- package/dist/collation.js +15 -0
- package/dist/generation/go.js +30 -2
- package/dist/generation/index.d.ts +1 -1
- package/dist/generation/python.js +24 -10
- package/dist/generation/swift.js +52 -3
- package/dist/generation/typescript.d.ts +18 -7
- package/dist/generation/typescript.js +122 -82
- package/dist/introspection/index.js +6 -23
- package/dist/introspection/relationships.d.ts +4 -4
- package/dist/introspection/relationships.js +4 -11
- package/dist/introspection/sql/columns.sql.d.ts +2 -7
- package/dist/introspection/sql/columns.sql.js +32 -9
- package/dist/introspection/sql/common.d.ts +7 -15
- package/dist/introspection/sql/foreign_tables.sql.d.ts +2 -6
- package/dist/introspection/sql/foreign_tables.sql.js +0 -5
- package/dist/introspection/sql/functions.sql.d.ts +2 -5
- package/dist/introspection/sql/functions.sql.js +0 -33
- package/dist/introspection/sql/helpers.d.ts +1 -1
- package/dist/introspection/sql/helpers.js +2 -2
- package/dist/introspection/sql/materialized_views.sql.d.ts +2 -4
- package/dist/introspection/sql/materialized_views.sql.js +0 -5
- package/dist/introspection/sql/pg-format.d.ts +21 -0
- package/dist/introspection/sql/pg-format.js +92 -0
- package/dist/introspection/sql/primary_keys.sql.d.ts +2 -4
- package/dist/introspection/sql/primary_keys.sql.js +0 -4
- package/dist/introspection/sql/schemas.sql.d.ts +2 -6
- package/dist/introspection/sql/schemas.sql.js +2 -6
- package/dist/introspection/sql/table.sql.d.ts +2 -4
- package/dist/introspection/sql/table.sql.js +0 -5
- package/dist/introspection/sql/table_relationships.sql.d.ts +2 -2
- package/dist/introspection/sql/types.sql.d.ts +5 -5
- package/dist/introspection/sql/types.sql.js +6 -17
- package/dist/introspection/sql/views.sql.d.ts +2 -4
- package/dist/introspection/sql/views.sql.js +6 -5
- package/dist/introspection/sql/views_key_dependencies.sql.d.ts +2 -2
- package/dist/introspection/sql/views_key_dependencies.sql.js +64 -75
- package/dist/sort.js +16 -15
- package/dist/types.d.ts +4 -0
- package/dist/types.js +11 -0
- package/package.json +3 -5
- package/src/collation.ts +16 -0
- package/src/generation/go.ts +31 -2
- package/src/generation/index.ts +1 -0
- package/src/generation/python.ts +25 -10
- package/src/generation/swift.ts +53 -3
- package/src/generation/typescript.ts +187 -107
- package/src/introspection/index.ts +7 -32
- package/src/introspection/relationships.ts +10 -19
- package/src/introspection/sql/columns.sql.ts +34 -18
- package/src/introspection/sql/common.ts +7 -17
- package/src/introspection/sql/foreign_tables.sql.ts +2 -13
- package/src/introspection/sql/functions.sql.ts +2 -44
- package/src/introspection/sql/helpers.ts +2 -2
- package/src/introspection/sql/materialized_views.sql.ts +2 -11
- package/src/introspection/sql/pg-format.ts +101 -0
- package/src/introspection/sql/primary_keys.sql.ts +2 -10
- package/src/introspection/sql/schemas.sql.ts +4 -14
- package/src/introspection/sql/table.sql.ts +2 -11
- package/src/introspection/sql/table_relationships.sql.ts +2 -4
- package/src/introspection/sql/types.sql.ts +6 -26
- package/src/introspection/sql/views.sql.ts +8 -11
- package/src/introspection/sql/views_key_dependencies.sql.ts +83 -77
- package/src/sort.ts +20 -15
- 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
|
|
180
|
-
introspectionBySchema[schema].views.sort((a, b) => a.view.name
|
|
181
|
-
introspectionBySchema[schema].functions.sort((a, b) => a.fn.name
|
|
182
|
-
introspectionBySchema[schema].enums.sort((a, b) => a.name
|
|
183
|
-
introspectionBySchema[schema].compositeTypes.sort((a, b) => a.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
|
-
|
|
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
|
-
},
|
|
480
|
+
}, typeContext)),
|
|
455
481
|
...schemaFunctions
|
|
456
|
-
.filter(({ fn }) => fn
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
537
|
+
}, typeContext)),
|
|
510
538
|
...schemaFunctions
|
|
511
|
-
.filter(({ fn }) => fn
|
|
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
|
-
${
|
|
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
|
-
},
|
|
558
|
+
}, typeContext);
|
|
528
559
|
})}
|
|
529
560
|
}
|
|
530
|
-
|
|
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
|
-
},
|
|
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
|
|
562
|
-
a.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,
|
|
725
|
-
// The schema that actually owns `pgType
|
|
726
|
-
//
|
|
727
|
-
// back to `schema.name` (the
|
|
728
|
-
//
|
|
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),
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
|
793
|
-
}
|
|
794
|
-
const
|
|
795
|
-
if (
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
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(
|
|
33
|
-
const plainFilter = filterByList(
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
-
*
|
|
76
|
-
*
|
|
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,
|
|
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 {
|
|
2
|
-
export declare const COLUMNS_SQL: (props:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 {
|
|
2
|
-
export declare const FOREIGN_TABLES_SQL: (props:
|
|
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 {
|
|
2
|
-
export declare const FUNCTIONS_SQL: (props:
|
|
3
|
-
nameFilter?: string;
|
|
4
|
-
args?: string[];
|
|
5
|
-
}) => string;
|
|
1
|
+
import type { SchemaFilterProps } from "./common.ts";
|
|
2
|
+
export declare const FUNCTIONS_SQL: (props: SchemaFilterProps) => string;
|