@supabase/postgrest-typegen 0.3.0 → 0.4.0
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 +4 -1
- package/dist/generation/go.js +3 -4
- package/dist/generation/python.js +3 -4
- package/dist/generation/swift.js +1 -1
- package/dist/generation/typescript.js +169 -146
- package/package.json +4 -4
- package/src/generation/go.ts +3 -4
- package/src/generation/python.ts +3 -4
- package/src/generation/swift.ts +1 -1
- package/src/generation/typescript.ts +166 -140
package/README.md
CHANGED
|
@@ -127,6 +127,9 @@ import {
|
|
|
127
127
|
| `generatePython` | — |
|
|
128
128
|
| `generateSwift` | `{ accessControl?: 'internal' \| 'public' \| 'private' \| 'package' }` |
|
|
129
129
|
|
|
130
|
+
Every generator returns the complete contents of the generated file, ending
|
|
131
|
+
with a single newline, so consumers write the result as-is.
|
|
132
|
+
|
|
130
133
|
`generateTypescript` formats its output with `oxfmt` unless `format` is
|
|
131
134
|
supplied. `oxfmt` is an optional peer dependency, imported only when that
|
|
132
135
|
default runs: install it to use the default formatter, or pass your own
|
|
@@ -148,7 +151,7 @@ npm install oxfmt@0.66.0
|
|
|
148
151
|
|
|
149
152
|
## Releasing
|
|
150
153
|
|
|
151
|
-
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.
|
|
154
|
+
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. Once npm serves the new version, the same workflow opens a pull request that bumps the exact pin in `packages/typegen`.
|
|
152
155
|
|
|
153
156
|
## License
|
|
154
157
|
|
package/dist/generation/go.js
CHANGED
|
@@ -8,8 +8,7 @@ export const generateGo = ({ schemas, tables, views, materializedViews, columns,
|
|
|
8
8
|
return acc;
|
|
9
9
|
}, {});
|
|
10
10
|
const compositeTypes = types.filter((type) => type.attributes.length > 0);
|
|
11
|
-
let output = `
|
|
12
|
-
package database
|
|
11
|
+
let output = `package database
|
|
13
12
|
|
|
14
13
|
${tables
|
|
15
14
|
.filter((table) => schemas.some((schema) => schema.name === table.schema))
|
|
@@ -30,8 +29,8 @@ ${compositeTypes
|
|
|
30
29
|
.filter((compositeType) => schemas.some((schema) => schema.name === compositeType.schema))
|
|
31
30
|
.map((compositeType) => generateCompositeTypeStruct(schemas.find((schema) => schema.name === compositeType.schema), compositeType, types))
|
|
32
31
|
.join("\n\n")}
|
|
33
|
-
`.
|
|
34
|
-
return output
|
|
32
|
+
`.trimEnd();
|
|
33
|
+
return `${output}\n`;
|
|
35
34
|
};
|
|
36
35
|
/**
|
|
37
36
|
* Converts a Postgres name to PascalCase.
|
|
@@ -16,8 +16,7 @@ export const generatePython = ({ schemas, tables, views, materializedViews, colu
|
|
|
16
16
|
.map((type) => ctx.typeToClass(type));
|
|
17
17
|
const py_views = views.map((view) => ctx.viewToClass(view));
|
|
18
18
|
const py_matviews = materializedViews.map((matview) => ctx.matViewToClass(matview));
|
|
19
|
-
let output = `
|
|
20
|
-
from __future__ import annotations
|
|
19
|
+
let output = `from __future__ import annotations
|
|
21
20
|
|
|
22
21
|
import datetime
|
|
23
22
|
import uuid
|
|
@@ -43,8 +42,8 @@ ${concatLines(py_matviews)}
|
|
|
43
42
|
|
|
44
43
|
${concatLines(composite_types)}
|
|
45
44
|
|
|
46
|
-
`.
|
|
47
|
-
return output
|
|
45
|
+
`.trimEnd();
|
|
46
|
+
return `${output}\n`;
|
|
48
47
|
};
|
|
49
48
|
class PythonContext {
|
|
50
49
|
types;
|
package/dist/generation/swift.js
CHANGED
|
@@ -216,7 +216,7 @@ export const generateSwift = (metadata, opts = {}) => {
|
|
|
216
216
|
];
|
|
217
217
|
}),
|
|
218
218
|
];
|
|
219
|
-
return output.join("\n")
|
|
219
|
+
return `${output.join("\n")}\n`;
|
|
220
220
|
};
|
|
221
221
|
// TODO: Make this more robust. Currently doesn't handle range types - returns them as string.
|
|
222
222
|
const pgTypeToSwiftType = (pgType, nullable, { types, views, tables, }) => {
|
|
@@ -359,6 +359,27 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
359
359
|
}
|
|
360
360
|
return null;
|
|
361
361
|
};
|
|
362
|
+
const getArgTsType = (schema, { name, type_id }) => {
|
|
363
|
+
const type = typesById.get(type_id);
|
|
364
|
+
if (!type) {
|
|
365
|
+
return "unknown";
|
|
366
|
+
}
|
|
367
|
+
const tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
368
|
+
// A row-typed parameter takes the relation's columns: PostgREST fills the
|
|
369
|
+
// keys it does not receive with NULL, and the computed fields are never
|
|
370
|
+
// input. A named parameter is typed without them. An unnamed one keeps the
|
|
371
|
+
// plain `Row` reference, which is how postgrest-js versions predating
|
|
372
|
+
// `ComputedFields` recognise the function as a computed field.
|
|
373
|
+
if (name === "") {
|
|
374
|
+
return tsType;
|
|
375
|
+
}
|
|
376
|
+
const relationReference = findRelationReference(type.name, typeContext, type.schema);
|
|
377
|
+
if (relationReference && tsType === `${relationReference}['Row']`) {
|
|
378
|
+
return `Omit<${tsType}, ${relationReference}['ComputedFields']>`;
|
|
379
|
+
}
|
|
380
|
+
return tsType;
|
|
381
|
+
};
|
|
382
|
+
const getArgsTsType = (schema, inArgs) => `{ ${inArgs.map((arg) => `${JSON.stringify(arg.name)}${arg.has_default ? "?" : ""}: ${getArgTsType(schema, arg)}`)} }`;
|
|
362
383
|
const getFunctionSignatures = (schema, fns) => {
|
|
363
384
|
return fns
|
|
364
385
|
.map(({ fn, inArgs }) => {
|
|
@@ -368,50 +389,19 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
368
389
|
// `never` property is uninhabited for any tool doing sound type math on the
|
|
369
390
|
// generated types. `Record<PropertyKey, never>` is the accurate type for
|
|
370
391
|
// "callable with no arguments".
|
|
371
|
-
|
|
392
|
+
const argsType = inArgs.length > 0
|
|
393
|
+
? getArgsTsType(schema, inArgs)
|
|
394
|
+
: "Record<PropertyKey, never>";
|
|
372
395
|
let returnType = getFunctionReturnType(schema, fn);
|
|
373
396
|
// Check for specific error cases
|
|
374
397
|
const conflictError = getConflictError(schema, fns, fn, inArgs);
|
|
375
398
|
if (conflictError) {
|
|
376
|
-
if (inArgs.length > 0) {
|
|
377
|
-
const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => {
|
|
378
|
-
const type = typesById.get(type_id);
|
|
379
|
-
let tsType = "unknown";
|
|
380
|
-
if (type) {
|
|
381
|
-
tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
382
|
-
}
|
|
383
|
-
return { name, type: tsType, has_default };
|
|
384
|
-
});
|
|
385
|
-
argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`)} }`;
|
|
386
|
-
}
|
|
387
399
|
returnType = `{ error: true } & ${JSON.stringify(conflictError)}`;
|
|
388
400
|
}
|
|
389
401
|
else if (hasTableRowError(fn, inArgs)) {
|
|
390
402
|
// Special case for computed fields returning scalars functions
|
|
391
|
-
if (inArgs.length > 0) {
|
|
392
|
-
const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => {
|
|
393
|
-
const type = typesById.get(type_id);
|
|
394
|
-
let tsType = "unknown";
|
|
395
|
-
if (type) {
|
|
396
|
-
tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
397
|
-
}
|
|
398
|
-
return { name, type: tsType, has_default };
|
|
399
|
-
});
|
|
400
|
-
argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`)} }`;
|
|
401
|
-
}
|
|
402
403
|
returnType = `{ error: true } & ${JSON.stringify(`the function ${schema.name}.${fn.name} with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache`)}`;
|
|
403
404
|
}
|
|
404
|
-
else if (inArgs.length > 0) {
|
|
405
|
-
const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => {
|
|
406
|
-
const type = typesById.get(type_id);
|
|
407
|
-
let tsType = "unknown";
|
|
408
|
-
if (type) {
|
|
409
|
-
tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
410
|
-
}
|
|
411
|
-
return { name, type: tsType, has_default };
|
|
412
|
-
});
|
|
413
|
-
argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`)} }`;
|
|
414
|
-
}
|
|
415
405
|
return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }`;
|
|
416
406
|
})
|
|
417
407
|
.join(" |\n");
|
|
@@ -471,6 +461,30 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
471
461
|
// field that resolved before keeps resolving.
|
|
472
462
|
return fn.argument_types === relation.name;
|
|
473
463
|
};
|
|
464
|
+
/**
|
|
465
|
+
* The computed fields of a relation, in `schemaFunctions` order. A function
|
|
466
|
+
* named like one of the relation's columns is left out: PostgREST resolves
|
|
467
|
+
* the name to the column, so it is neither selectable nor part of `Row`.
|
|
468
|
+
*/
|
|
469
|
+
const computedFieldsOf = (schemaFunctions, relation) => {
|
|
470
|
+
const columnNames = new Set(getColumns(relation.id).map((column) => column.name));
|
|
471
|
+
const seen = new Set();
|
|
472
|
+
return schemaFunctions.filter(({ fn, inArgs }) => {
|
|
473
|
+
if (!isComputedFieldOf(fn, inArgs, relation) ||
|
|
474
|
+
columnNames.has(fn.name) ||
|
|
475
|
+
seen.has(fn.name)) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
seen.add(fn.name);
|
|
479
|
+
return true;
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
const computedFieldTsDefinition = (schema, { fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`;
|
|
483
|
+
// `ComputedFields` names the `Row` keys PostgREST leaves out of `select('*')`,
|
|
484
|
+
// so postgrest-js does not have to infer them from `Functions`.
|
|
485
|
+
const computedFieldsTsType = (computedFields) => computedFields.length === 0
|
|
486
|
+
? "never"
|
|
487
|
+
: computedFields.map(({ fn }) => JSON.stringify(fn.name)).join(" | ");
|
|
474
488
|
let output = `
|
|
475
489
|
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
|
|
476
490
|
|
|
@@ -482,117 +496,119 @@ export type Database = {
|
|
|
482
496
|
Tables: {
|
|
483
497
|
${schemaTables.length === 0
|
|
484
498
|
? "[_ in never]: never"
|
|
485
|
-
: schemaTables.map(({ table, relationships
|
|
499
|
+
: schemaTables.map(({ table, relationships }) => {
|
|
500
|
+
const computedFields = computedFieldsOf(schemaFunctions, table);
|
|
501
|
+
return `${JSON.stringify(table.name)}: {
|
|
486
502
|
Row: {
|
|
487
503
|
${[
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
.map(({ fn }) => {
|
|
498
|
-
return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`;
|
|
499
|
-
}),
|
|
500
|
-
]}
|
|
504
|
+
...getColumns(table.id).map((column) => generateColumnTsDefinition(schema, {
|
|
505
|
+
name: column.name,
|
|
506
|
+
format: column.format,
|
|
507
|
+
type_schema: column.type_schema,
|
|
508
|
+
is_nullable: column.is_nullable,
|
|
509
|
+
is_optional: false,
|
|
510
|
+
}, typeContext)),
|
|
511
|
+
...computedFields.map((computedField) => computedFieldTsDefinition(schema, computedField)),
|
|
512
|
+
]}
|
|
501
513
|
}
|
|
514
|
+
ComputedFields: ${computedFieldsTsType(computedFields)}
|
|
502
515
|
Insert: {
|
|
503
516
|
${getColumns(table.id).map((column) => {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
517
|
+
if (column.identity_generation === "ALWAYS" ||
|
|
518
|
+
column.is_generated) {
|
|
519
|
+
return `${JSON.stringify(column.name)}?: never`;
|
|
520
|
+
}
|
|
521
|
+
return generateColumnTsDefinition(schema, {
|
|
522
|
+
name: column.name,
|
|
523
|
+
format: column.format,
|
|
524
|
+
type_schema: column.type_schema,
|
|
525
|
+
is_nullable: column.is_nullable,
|
|
526
|
+
is_optional: column.is_nullable ||
|
|
527
|
+
column.is_identity ||
|
|
528
|
+
column.default_value !== null,
|
|
529
|
+
}, typeContext);
|
|
530
|
+
})}
|
|
518
531
|
}
|
|
519
532
|
Update: {
|
|
520
533
|
${getColumns(table.id).map((column) => {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
534
|
+
if (column.identity_generation === "ALWAYS" ||
|
|
535
|
+
column.is_generated) {
|
|
536
|
+
return `${JSON.stringify(column.name)}?: never`;
|
|
537
|
+
}
|
|
538
|
+
return generateColumnTsDefinition(schema, {
|
|
539
|
+
name: column.name,
|
|
540
|
+
format: column.format,
|
|
541
|
+
type_schema: column.type_schema,
|
|
542
|
+
is_nullable: column.is_nullable,
|
|
543
|
+
is_optional: true,
|
|
544
|
+
}, typeContext);
|
|
545
|
+
})}
|
|
533
546
|
}
|
|
534
547
|
Relationships: [
|
|
535
548
|
${relationships.map(generateRelationshiptTsDefinition)}
|
|
536
549
|
]
|
|
537
|
-
}
|
|
550
|
+
}`;
|
|
551
|
+
})}
|
|
538
552
|
}
|
|
539
553
|
Views: {
|
|
540
554
|
${schemaViews.length === 0
|
|
541
555
|
? "[_ in never]: never"
|
|
542
|
-
: schemaViews.map(({ view, relationships
|
|
556
|
+
: schemaViews.map(({ view, relationships }) => {
|
|
557
|
+
const computedFields = computedFieldsOf(schemaFunctions, view);
|
|
558
|
+
return `${JSON.stringify(view.name)}: {
|
|
543
559
|
Row: {
|
|
544
560
|
${[
|
|
545
|
-
|
|
546
|
-
name: column.name,
|
|
547
|
-
format: column.format,
|
|
548
|
-
type_schema: column.type_schema,
|
|
549
|
-
is_nullable: column.is_nullable,
|
|
550
|
-
is_optional: false,
|
|
551
|
-
}, typeContext)),
|
|
552
|
-
...schemaFunctions
|
|
553
|
-
.filter(({ fn, inArgs }) => isComputedFieldOf(fn, inArgs, view))
|
|
554
|
-
.map(({ fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`),
|
|
555
|
-
]}
|
|
556
|
-
}
|
|
557
|
-
${
|
|
558
|
-
// Metadata predating the trigger-aware flags (still valid
|
|
559
|
-
// version 1 documents) falls back to the old gate.
|
|
560
|
-
(view.is_insert_enabled ?? view.is_updatable)
|
|
561
|
-
? `Insert: {
|
|
562
|
-
${getColumns(view.id).map((column) => {
|
|
563
|
-
if (!column.is_updatable) {
|
|
564
|
-
return `${JSON.stringify(column.name)}?: never`;
|
|
565
|
-
}
|
|
566
|
-
return generateColumnTsDefinition(schema, {
|
|
561
|
+
...getColumns(view.id).map((column) => generateColumnTsDefinition(schema, {
|
|
567
562
|
name: column.name,
|
|
568
563
|
format: column.format,
|
|
569
564
|
type_schema: column.type_schema,
|
|
570
|
-
is_nullable:
|
|
571
|
-
is_optional:
|
|
572
|
-
}, typeContext)
|
|
573
|
-
|
|
565
|
+
is_nullable: column.is_nullable,
|
|
566
|
+
is_optional: false,
|
|
567
|
+
}, typeContext)),
|
|
568
|
+
...computedFields.map((computedField) => computedFieldTsDefinition(schema, computedField)),
|
|
569
|
+
]}
|
|
570
|
+
}
|
|
571
|
+
ComputedFields: ${computedFieldsTsType(computedFields)}
|
|
572
|
+
${
|
|
573
|
+
// Metadata predating the trigger-aware flags (still valid
|
|
574
|
+
// version 1 documents) falls back to the old gate.
|
|
575
|
+
(view.is_insert_enabled ?? view.is_updatable)
|
|
576
|
+
? `Insert: {
|
|
577
|
+
${getColumns(view.id).map((column) => {
|
|
578
|
+
if (!column.is_updatable) {
|
|
579
|
+
return `${JSON.stringify(column.name)}?: never`;
|
|
580
|
+
}
|
|
581
|
+
return generateColumnTsDefinition(schema, {
|
|
582
|
+
name: column.name,
|
|
583
|
+
format: column.format,
|
|
584
|
+
type_schema: column.type_schema,
|
|
585
|
+
is_nullable: true,
|
|
586
|
+
is_optional: true,
|
|
587
|
+
}, typeContext);
|
|
588
|
+
})}
|
|
574
589
|
}
|
|
575
590
|
`
|
|
576
|
-
|
|
577
|
-
|
|
591
|
+
: ""}${(view.is_update_enabled ?? view.is_updatable)
|
|
592
|
+
? `Update: {
|
|
578
593
|
${getColumns(view.id).map((column) => {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
594
|
+
if (!column.is_updatable) {
|
|
595
|
+
return `${JSON.stringify(column.name)}?: never`;
|
|
596
|
+
}
|
|
597
|
+
return generateColumnTsDefinition(schema, {
|
|
598
|
+
name: column.name,
|
|
599
|
+
format: column.format,
|
|
600
|
+
type_schema: column.type_schema,
|
|
601
|
+
is_nullable: true,
|
|
602
|
+
is_optional: true,
|
|
603
|
+
}, typeContext);
|
|
604
|
+
})}
|
|
590
605
|
}
|
|
591
606
|
`
|
|
592
|
-
|
|
607
|
+
: ""}Relationships: [
|
|
593
608
|
${relationships.map(generateRelationshiptTsDefinition)}
|
|
594
609
|
]
|
|
595
|
-
}
|
|
610
|
+
}`;
|
|
611
|
+
})}
|
|
596
612
|
}
|
|
597
613
|
Functions: {
|
|
598
614
|
${(() => {
|
|
@@ -769,7 +785,7 @@ export const pgTypeToTsType = (schema, pgType, context,
|
|
|
769
785
|
// schema being generated) when not provided, which is only a guess, so pass
|
|
770
786
|
// it whenever the owning schema is known.
|
|
771
787
|
typeSchema) => {
|
|
772
|
-
const { types, schemas
|
|
788
|
+
const { types, schemas } = context;
|
|
773
789
|
if (pgType === "bool") {
|
|
774
790
|
return "boolean";
|
|
775
791
|
}
|
|
@@ -824,34 +840,41 @@ typeSchema) => {
|
|
|
824
840
|
}
|
|
825
841
|
return "unknown";
|
|
826
842
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
843
|
+
const relationReference = findRelationReference(pgType, context, preferredSchema);
|
|
844
|
+
return relationReference ? `${relationReference}['Row']` : "unknown";
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
/**
|
|
848
|
+
* The `Database[schema][kind][name]` reference of the relation whose row type
|
|
849
|
+
* is `pgType`, or `undefined` when no generated relation has that type.
|
|
850
|
+
*
|
|
851
|
+
* A relation-typed value carries only a bare type name, so several relations
|
|
852
|
+
* can share it and `preferredSchema` is what disambiguates them. Match on the
|
|
853
|
+
* schema across every relation kind before falling back to kind order,
|
|
854
|
+
* otherwise a relation in an unrelated schema outranks the one the caller
|
|
855
|
+
* meant. Foreign tables are generated under `Tables` and materialized views
|
|
856
|
+
* under `Views`, alongside their plain counterparts.
|
|
857
|
+
*/
|
|
858
|
+
const findRelationReference = (pgType, context, preferredSchema) => {
|
|
859
|
+
const { schemas, tables, views, foreignTables = [], materializedViews = [], } = context;
|
|
860
|
+
const relationKinds = [
|
|
861
|
+
["Tables", tables],
|
|
862
|
+
["Tables", foreignTables],
|
|
863
|
+
["Views", views],
|
|
864
|
+
["Views", materializedViews],
|
|
865
|
+
];
|
|
866
|
+
const findRelation = (matches) => {
|
|
867
|
+
for (const [kind, relations] of relationKinds) {
|
|
868
|
+
const relation = relations.find(matches);
|
|
869
|
+
if (relation)
|
|
870
|
+
return { kind, relation };
|
|
854
871
|
}
|
|
855
|
-
return
|
|
872
|
+
return undefined;
|
|
873
|
+
};
|
|
874
|
+
const match = findRelation(({ name, schema: relationSchema }) => name === pgType && relationSchema === preferredSchema) ?? findRelation(({ name }) => name === pgType);
|
|
875
|
+
if (!match || !schemas.some(({ name }) => name === match.relation.schema)) {
|
|
876
|
+
return undefined;
|
|
856
877
|
}
|
|
878
|
+
const { kind, relation } = match;
|
|
879
|
+
return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(relation.name)}]`;
|
|
857
880
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/postgrest-typegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Type generation for PostgREST from PostgreSQL schemas",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codegen",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"test": "bun scripts/run-tests.ts test/"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"arktype": "2.2.
|
|
65
|
+
"arktype": "2.2.5"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testcontainers/postgresql": "^12.1.0",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"@types/pg": "^8.23.1",
|
|
75
75
|
"knip": "^6.32.3",
|
|
76
76
|
"oxfmt": "0.66.0",
|
|
77
|
-
"oxlint": "1.
|
|
78
|
-
"oxlint-tsgolint": "7.0.
|
|
77
|
+
"oxlint": "1.85.0",
|
|
78
|
+
"oxlint-tsgolint": "7.0.2003",
|
|
79
79
|
"pg": "^8.23.0",
|
|
80
80
|
"testcontainers": "^12.1.0",
|
|
81
81
|
"typescript": "7.0.2"
|
package/src/generation/go.ts
CHANGED
|
@@ -32,8 +32,7 @@ export const generateGo = ({
|
|
|
32
32
|
|
|
33
33
|
const compositeTypes = types.filter((type) => type.attributes.length > 0);
|
|
34
34
|
|
|
35
|
-
let output = `
|
|
36
|
-
package database
|
|
35
|
+
let output = `package database
|
|
37
36
|
|
|
38
37
|
${tables
|
|
39
38
|
.filter((table) => schemas.some((schema) => schema.name === table.schema))
|
|
@@ -88,9 +87,9 @@ ${compositeTypes
|
|
|
88
87
|
),
|
|
89
88
|
)
|
|
90
89
|
.join("\n\n")}
|
|
91
|
-
`.
|
|
90
|
+
`.trimEnd();
|
|
92
91
|
|
|
93
|
-
return output
|
|
92
|
+
return `${output}\n`;
|
|
94
93
|
};
|
|
95
94
|
|
|
96
95
|
/**
|
package/src/generation/python.ts
CHANGED
|
@@ -38,8 +38,7 @@ export const generatePython = ({
|
|
|
38
38
|
ctx.matViewToClass(matview),
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
-
let output = `
|
|
42
|
-
from __future__ import annotations
|
|
41
|
+
let output = `from __future__ import annotations
|
|
43
42
|
|
|
44
43
|
import datetime
|
|
45
44
|
import uuid
|
|
@@ -65,9 +64,9 @@ ${concatLines(py_matviews)}
|
|
|
65
64
|
|
|
66
65
|
${concatLines(composite_types)}
|
|
67
66
|
|
|
68
|
-
`.
|
|
67
|
+
`.trimEnd();
|
|
69
68
|
|
|
70
|
-
return output
|
|
69
|
+
return `${output}\n`;
|
|
71
70
|
};
|
|
72
71
|
|
|
73
72
|
interface Serializable {
|
package/src/generation/swift.ts
CHANGED
|
@@ -584,6 +584,43 @@ export const generateTypescript = async (
|
|
|
584
584
|
return null;
|
|
585
585
|
};
|
|
586
586
|
|
|
587
|
+
const getArgTsType = (
|
|
588
|
+
schema: PostgresSchema,
|
|
589
|
+
{ name, type_id }: PostgresFunction["args"][number],
|
|
590
|
+
) => {
|
|
591
|
+
const type = typesById.get(type_id);
|
|
592
|
+
if (!type) {
|
|
593
|
+
return "unknown";
|
|
594
|
+
}
|
|
595
|
+
const tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
596
|
+
// A row-typed parameter takes the relation's columns: PostgREST fills the
|
|
597
|
+
// keys it does not receive with NULL, and the computed fields are never
|
|
598
|
+
// input. A named parameter is typed without them. An unnamed one keeps the
|
|
599
|
+
// plain `Row` reference, which is how postgrest-js versions predating
|
|
600
|
+
// `ComputedFields` recognise the function as a computed field.
|
|
601
|
+
if (name === "") {
|
|
602
|
+
return tsType;
|
|
603
|
+
}
|
|
604
|
+
const relationReference = findRelationReference(
|
|
605
|
+
type.name,
|
|
606
|
+
typeContext,
|
|
607
|
+
type.schema,
|
|
608
|
+
);
|
|
609
|
+
if (relationReference && tsType === `${relationReference}['Row']`) {
|
|
610
|
+
return `Omit<${tsType}, ${relationReference}['ComputedFields']>`;
|
|
611
|
+
}
|
|
612
|
+
return tsType;
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const getArgsTsType = (
|
|
616
|
+
schema: PostgresSchema,
|
|
617
|
+
inArgs: PostgresFunction["args"],
|
|
618
|
+
) =>
|
|
619
|
+
`{ ${inArgs.map(
|
|
620
|
+
(arg) =>
|
|
621
|
+
`${JSON.stringify(arg.name)}${arg.has_default ? "?" : ""}: ${getArgTsType(schema, arg)}`,
|
|
622
|
+
)} }`;
|
|
623
|
+
|
|
587
624
|
const getFunctionSignatures = (
|
|
588
625
|
schema: PostgresSchema,
|
|
589
626
|
fns: Array<{ fn: PostgresFunction; inArgs: PostgresFunction["args"] }>,
|
|
@@ -596,80 +633,21 @@ export const generateTypescript = async (
|
|
|
596
633
|
// `never` property is uninhabited for any tool doing sound type math on the
|
|
597
634
|
// generated types. `Record<PropertyKey, never>` is the accurate type for
|
|
598
635
|
// "callable with no arguments".
|
|
599
|
-
|
|
636
|
+
const argsType =
|
|
637
|
+
inArgs.length > 0
|
|
638
|
+
? getArgsTsType(schema, inArgs)
|
|
639
|
+
: "Record<PropertyKey, never>";
|
|
600
640
|
let returnType = getFunctionReturnType(schema, fn);
|
|
601
641
|
|
|
602
642
|
// Check for specific error cases
|
|
603
643
|
const conflictError = getConflictError(schema, fns, fn, inArgs);
|
|
604
644
|
if (conflictError) {
|
|
605
|
-
if (inArgs.length > 0) {
|
|
606
|
-
const argsNameAndType = inArgs.map(
|
|
607
|
-
({ name, type_id, has_default }) => {
|
|
608
|
-
const type = typesById.get(type_id);
|
|
609
|
-
let tsType = "unknown";
|
|
610
|
-
if (type) {
|
|
611
|
-
tsType = pgTypeToTsType(
|
|
612
|
-
schema,
|
|
613
|
-
type.name,
|
|
614
|
-
typeContext,
|
|
615
|
-
type.schema,
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
return { name, type: tsType, has_default };
|
|
619
|
-
},
|
|
620
|
-
);
|
|
621
|
-
argsType = `{ ${argsNameAndType.map(
|
|
622
|
-
({ name, type, has_default }) =>
|
|
623
|
-
`${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`,
|
|
624
|
-
)} }`;
|
|
625
|
-
}
|
|
626
645
|
returnType = `{ error: true } & ${JSON.stringify(conflictError)}`;
|
|
627
646
|
} else if (hasTableRowError(fn, inArgs)) {
|
|
628
647
|
// Special case for computed fields returning scalars functions
|
|
629
|
-
if (inArgs.length > 0) {
|
|
630
|
-
const argsNameAndType = inArgs.map(
|
|
631
|
-
({ name, type_id, has_default }) => {
|
|
632
|
-
const type = typesById.get(type_id);
|
|
633
|
-
let tsType = "unknown";
|
|
634
|
-
if (type) {
|
|
635
|
-
tsType = pgTypeToTsType(
|
|
636
|
-
schema,
|
|
637
|
-
type.name,
|
|
638
|
-
typeContext,
|
|
639
|
-
type.schema,
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
return { name, type: tsType, has_default };
|
|
643
|
-
},
|
|
644
|
-
);
|
|
645
|
-
argsType = `{ ${argsNameAndType.map(
|
|
646
|
-
({ name, type, has_default }) =>
|
|
647
|
-
`${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`,
|
|
648
|
-
)} }`;
|
|
649
|
-
}
|
|
650
648
|
returnType = `{ error: true } & ${JSON.stringify(
|
|
651
649
|
`the function ${schema.name}.${fn.name} with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache`,
|
|
652
650
|
)}`;
|
|
653
|
-
} else if (inArgs.length > 0) {
|
|
654
|
-
const argsNameAndType = inArgs.map(
|
|
655
|
-
({ name, type_id, has_default }) => {
|
|
656
|
-
const type = typesById.get(type_id);
|
|
657
|
-
let tsType = "unknown";
|
|
658
|
-
if (type) {
|
|
659
|
-
tsType = pgTypeToTsType(
|
|
660
|
-
schema,
|
|
661
|
-
type.name,
|
|
662
|
-
typeContext,
|
|
663
|
-
type.schema,
|
|
664
|
-
);
|
|
665
|
-
}
|
|
666
|
-
return { name, type: tsType, has_default };
|
|
667
|
-
},
|
|
668
|
-
);
|
|
669
|
-
argsType = `{ ${argsNameAndType.map(
|
|
670
|
-
({ name, type, has_default }) =>
|
|
671
|
-
`${JSON.stringify(name)}${has_default ? "?" : ""}: ${type}`,
|
|
672
|
-
)} }`;
|
|
673
651
|
}
|
|
674
652
|
|
|
675
653
|
return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }`;
|
|
@@ -753,6 +731,51 @@ export const generateTypescript = async (
|
|
|
753
731
|
return fn.argument_types === relation.name;
|
|
754
732
|
};
|
|
755
733
|
|
|
734
|
+
/**
|
|
735
|
+
* The computed fields of a relation, in `schemaFunctions` order. A function
|
|
736
|
+
* named like one of the relation's columns is left out: PostgREST resolves
|
|
737
|
+
* the name to the column, so it is neither selectable nor part of `Row`.
|
|
738
|
+
*/
|
|
739
|
+
const computedFieldsOf = (
|
|
740
|
+
schemaFunctions: {
|
|
741
|
+
fn: PostgresFunction;
|
|
742
|
+
inArgs: PostgresFunction["args"];
|
|
743
|
+
}[],
|
|
744
|
+
relation: { id: number; name: string },
|
|
745
|
+
) => {
|
|
746
|
+
const columnNames = new Set(
|
|
747
|
+
getColumns(relation.id).map((column) => column.name),
|
|
748
|
+
);
|
|
749
|
+
const seen = new Set<string>();
|
|
750
|
+
return schemaFunctions.filter(({ fn, inArgs }) => {
|
|
751
|
+
if (
|
|
752
|
+
!isComputedFieldOf(fn, inArgs, relation) ||
|
|
753
|
+
columnNames.has(fn.name) ||
|
|
754
|
+
seen.has(fn.name)
|
|
755
|
+
) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
seen.add(fn.name);
|
|
759
|
+
return true;
|
|
760
|
+
});
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const computedFieldTsDefinition = (
|
|
764
|
+
schema: PostgresSchema,
|
|
765
|
+
{ fn }: { fn: PostgresFunction },
|
|
766
|
+
) =>
|
|
767
|
+
`${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(
|
|
768
|
+
getFunctionReturnType(schema, fn),
|
|
769
|
+
true,
|
|
770
|
+
)}`;
|
|
771
|
+
|
|
772
|
+
// `ComputedFields` names the `Row` keys PostgREST leaves out of `select('*')`,
|
|
773
|
+
// so postgrest-js does not have to infer them from `Functions`.
|
|
774
|
+
const computedFieldsTsType = (computedFields: { fn: PostgresFunction }[]) =>
|
|
775
|
+
computedFields.length === 0
|
|
776
|
+
? "never"
|
|
777
|
+
: computedFields.map(({ fn }) => JSON.stringify(fn.name)).join(" | ");
|
|
778
|
+
|
|
756
779
|
let output = `
|
|
757
780
|
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
|
|
758
781
|
|
|
@@ -771,11 +794,12 @@ export type Database = {
|
|
|
771
794
|
${
|
|
772
795
|
schemaTables.length === 0
|
|
773
796
|
? "[_ in never]: never"
|
|
774
|
-
: schemaTables.map(
|
|
775
|
-
(
|
|
797
|
+
: schemaTables.map(({ table, relationships }) => {
|
|
798
|
+
const computedFields = computedFieldsOf(
|
|
799
|
+
schemaFunctions,
|
|
776
800
|
table,
|
|
777
|
-
|
|
778
|
-
|
|
801
|
+
);
|
|
802
|
+
return `${JSON.stringify(table.name)}: {
|
|
779
803
|
Row: {
|
|
780
804
|
${[
|
|
781
805
|
...getColumns(table.id).map((column) =>
|
|
@@ -791,18 +815,12 @@ export type Database = {
|
|
|
791
815
|
typeContext,
|
|
792
816
|
),
|
|
793
817
|
),
|
|
794
|
-
...
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
)
|
|
798
|
-
.map(({ fn }) => {
|
|
799
|
-
return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(
|
|
800
|
-
getFunctionReturnType(schema, fn),
|
|
801
|
-
true,
|
|
802
|
-
)}`;
|
|
803
|
-
}),
|
|
818
|
+
...computedFields.map((computedField) =>
|
|
819
|
+
computedFieldTsDefinition(schema, computedField),
|
|
820
|
+
),
|
|
804
821
|
]}
|
|
805
822
|
}
|
|
823
|
+
ComputedFields: ${computedFieldsTsType(computedFields)}
|
|
806
824
|
Insert: {
|
|
807
825
|
${getColumns(table.id).map((column) => {
|
|
808
826
|
if (
|
|
@@ -852,19 +870,20 @@ export type Database = {
|
|
|
852
870
|
Relationships: [
|
|
853
871
|
${relationships.map(generateRelationshiptTsDefinition)}
|
|
854
872
|
]
|
|
855
|
-
}
|
|
856
|
-
)
|
|
873
|
+
}`;
|
|
874
|
+
})
|
|
857
875
|
}
|
|
858
876
|
}
|
|
859
877
|
Views: {
|
|
860
878
|
${
|
|
861
879
|
schemaViews.length === 0
|
|
862
880
|
? "[_ in never]: never"
|
|
863
|
-
: schemaViews.map(
|
|
864
|
-
(
|
|
881
|
+
: schemaViews.map(({ view, relationships }) => {
|
|
882
|
+
const computedFields = computedFieldsOf(
|
|
883
|
+
schemaFunctions,
|
|
865
884
|
view,
|
|
866
|
-
|
|
867
|
-
|
|
885
|
+
);
|
|
886
|
+
return `${JSON.stringify(view.name)}: {
|
|
868
887
|
Row: {
|
|
869
888
|
${[
|
|
870
889
|
...getColumns(view.id).map((column) =>
|
|
@@ -880,19 +899,12 @@ export type Database = {
|
|
|
880
899
|
typeContext,
|
|
881
900
|
),
|
|
882
901
|
),
|
|
883
|
-
...
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
)
|
|
887
|
-
.map(
|
|
888
|
-
({ fn }) =>
|
|
889
|
-
`${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(
|
|
890
|
-
getFunctionReturnType(schema, fn),
|
|
891
|
-
true,
|
|
892
|
-
)}`,
|
|
893
|
-
),
|
|
902
|
+
...computedFields.map((computedField) =>
|
|
903
|
+
computedFieldTsDefinition(schema, computedField),
|
|
904
|
+
),
|
|
894
905
|
]}
|
|
895
906
|
}
|
|
907
|
+
ComputedFields: ${computedFieldsTsType(computedFields)}
|
|
896
908
|
${
|
|
897
909
|
// Metadata predating the trigger-aware flags (still valid
|
|
898
910
|
// version 1 documents) falls back to the old gate.
|
|
@@ -942,8 +954,8 @@ export type Database = {
|
|
|
942
954
|
}Relationships: [
|
|
943
955
|
${relationships.map(generateRelationshiptTsDefinition)}
|
|
944
956
|
]
|
|
945
|
-
}
|
|
946
|
-
)
|
|
957
|
+
}`;
|
|
958
|
+
})
|
|
947
959
|
}
|
|
948
960
|
}
|
|
949
961
|
Functions: {
|
|
@@ -1157,14 +1169,7 @@ export const pgTypeToTsType = (
|
|
|
1157
1169
|
// it whenever the owning schema is known.
|
|
1158
1170
|
typeSchema?: string,
|
|
1159
1171
|
): string => {
|
|
1160
|
-
const {
|
|
1161
|
-
types,
|
|
1162
|
-
schemas,
|
|
1163
|
-
tables,
|
|
1164
|
-
views,
|
|
1165
|
-
foreignTables = [],
|
|
1166
|
-
materializedViews = [],
|
|
1167
|
-
} = context;
|
|
1172
|
+
const { types, schemas } = context;
|
|
1168
1173
|
if (pgType === "bool") {
|
|
1169
1174
|
return "boolean";
|
|
1170
1175
|
} else if (
|
|
@@ -1228,42 +1233,63 @@ export const pgTypeToTsType = (
|
|
|
1228
1233
|
return "unknown";
|
|
1229
1234
|
}
|
|
1230
1235
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
["Tables", foreignTables],
|
|
1240
|
-
["Views", views],
|
|
1241
|
-
["Views", materializedViews],
|
|
1242
|
-
] as const;
|
|
1243
|
-
const findRelation = (
|
|
1244
|
-
matches: (relation: { name: string; schema: string }) => boolean,
|
|
1245
|
-
) => {
|
|
1246
|
-
for (const [kind, relations] of relationKinds) {
|
|
1247
|
-
const relation = relations.find(matches);
|
|
1248
|
-
if (relation) return { kind, relation };
|
|
1249
|
-
}
|
|
1250
|
-
return undefined;
|
|
1251
|
-
};
|
|
1252
|
-
const match =
|
|
1253
|
-
findRelation(
|
|
1254
|
-
({ name, schema: relationSchema }) =>
|
|
1255
|
-
name === pgType && relationSchema === preferredSchema,
|
|
1256
|
-
) ?? findRelation(({ name }) => name === pgType);
|
|
1257
|
-
if (match) {
|
|
1258
|
-
const { kind, relation } = match;
|
|
1259
|
-
if (schemas.some(({ name }) => name === relation.schema)) {
|
|
1260
|
-
return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(
|
|
1261
|
-
relation.name,
|
|
1262
|
-
)}]['Row']`;
|
|
1263
|
-
}
|
|
1264
|
-
return "unknown";
|
|
1265
|
-
}
|
|
1236
|
+
const relationReference = findRelationReference(
|
|
1237
|
+
pgType,
|
|
1238
|
+
context,
|
|
1239
|
+
preferredSchema,
|
|
1240
|
+
);
|
|
1241
|
+
return relationReference ? `${relationReference}['Row']` : "unknown";
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1266
1244
|
|
|
1267
|
-
|
|
1245
|
+
/**
|
|
1246
|
+
* The `Database[schema][kind][name]` reference of the relation whose row type
|
|
1247
|
+
* is `pgType`, or `undefined` when no generated relation has that type.
|
|
1248
|
+
*
|
|
1249
|
+
* A relation-typed value carries only a bare type name, so several relations
|
|
1250
|
+
* can share it and `preferredSchema` is what disambiguates them. Match on the
|
|
1251
|
+
* schema across every relation kind before falling back to kind order,
|
|
1252
|
+
* otherwise a relation in an unrelated schema outranks the one the caller
|
|
1253
|
+
* meant. Foreign tables are generated under `Tables` and materialized views
|
|
1254
|
+
* under `Views`, alongside their plain counterparts.
|
|
1255
|
+
*/
|
|
1256
|
+
const findRelationReference = (
|
|
1257
|
+
pgType: string,
|
|
1258
|
+
context: TypeResolutionContext,
|
|
1259
|
+
preferredSchema: string,
|
|
1260
|
+
): string | undefined => {
|
|
1261
|
+
const {
|
|
1262
|
+
schemas,
|
|
1263
|
+
tables,
|
|
1264
|
+
views,
|
|
1265
|
+
foreignTables = [],
|
|
1266
|
+
materializedViews = [],
|
|
1267
|
+
} = context;
|
|
1268
|
+
const relationKinds = [
|
|
1269
|
+
["Tables", tables],
|
|
1270
|
+
["Tables", foreignTables],
|
|
1271
|
+
["Views", views],
|
|
1272
|
+
["Views", materializedViews],
|
|
1273
|
+
] as const;
|
|
1274
|
+
const findRelation = (
|
|
1275
|
+
matches: (relation: { name: string; schema: string }) => boolean,
|
|
1276
|
+
) => {
|
|
1277
|
+
for (const [kind, relations] of relationKinds) {
|
|
1278
|
+
const relation = relations.find(matches);
|
|
1279
|
+
if (relation) return { kind, relation };
|
|
1280
|
+
}
|
|
1281
|
+
return undefined;
|
|
1282
|
+
};
|
|
1283
|
+
const match =
|
|
1284
|
+
findRelation(
|
|
1285
|
+
({ name, schema: relationSchema }) =>
|
|
1286
|
+
name === pgType && relationSchema === preferredSchema,
|
|
1287
|
+
) ?? findRelation(({ name }) => name === pgType);
|
|
1288
|
+
if (!match || !schemas.some(({ name }) => name === match.relation.schema)) {
|
|
1289
|
+
return undefined;
|
|
1268
1290
|
}
|
|
1291
|
+
const { kind, relation } = match;
|
|
1292
|
+
return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(
|
|
1293
|
+
relation.name,
|
|
1294
|
+
)}]`;
|
|
1269
1295
|
};
|