Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 59 additions & 6 deletions packages/supabase_typegen/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,63 @@
# supabase_typegen

> [!WARNING]
> This is a placeholder release that reserves the package name on pub.dev. The
> code generator is still under development and this version does nothing yet.
Generates typed Supabase table definitions from your database schema, so
query results never expose raw `Map<String, dynamic>` data.

A command-line code generator that turns a Supabase database schema into typed
Dart table definitions for use with the Supabase client packages.
For every table the generator emits:

The generator implementation will land in a future release.
- a zero-cost row extension type over the decoded JSON map with typed getters,
- `Insert` and `Update` value types that enforce required columns at the
construction site,
- a `PostgrestTable` definition and `TableColumn` tokens for compile-time
checked filters,
- Dart enums for Postgres enums, with wire-name mapping.

## Usage

```sh
dart run supabase_typegen \
--url https://your-project.supabase.co \
--key $SUPABASE_ANON_KEY \
--output lib/supabase_schema.g.dart
```

`--url` and `--key` fall back to the `SUPABASE_URL` and `SUPABASE_ANON_KEY`
environment variables. Use `--schema` to generate for a schema other than
`public`, and `--import` to change which library the generated file imports
`PostgrestTable` and `TableColumn` from.

The schema is read from the OpenAPI description that PostgREST serves at the
API root, so the key only needs read access; tables whose role lacks
privileges (grants, not row level security) are not included.

## Generated code in action

```dart
final books = await client.table(Books.table)
.select()
.where(Books.mood.eq(Mood.happy))
.order(Books.createdAt, ascending: false); // List<BooksRow>

await client.table(Books.table).insert(
BooksInsert(title: 'A typed row', tags: ['dart']),
);
```

## Known limitations

- The OpenAPI description does not distinguish nullable columns from
`NOT NULL` columns with a database default, so getters for defaulted
columns other than primary keys are conservatively nullable.
- Passing `null` to an `Insert`/`Update` parameter omits the column. To write
SQL NULL explicitly, use the generated `set…ToNull` methods, for example
`BooksUpdate(inPrint: false).setPriceToNull()`; they only exist for
nullable columns, so nulling a `NOT NULL` column is a compile error.
- Array elements are assumed non-null (`text[]` maps to `List<String>`),
matching the supabase-js type generator; arrays containing SQL NULL
elements throw when the element is read. Enum array columns degrade to
`List<String>`.
- `timestamptz` values are written back in UTC, naive `timestamp` values as
local wall time, and `date` values date-only, so calendar dates never
shift with the client timezone.
- Foreign key relationship getters and typed functions (rpc) are not
generated yet.
138 changes: 138 additions & 0 deletions packages/supabase_typegen/bin/supabase_typegen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';
import 'package:http/http.dart' as http;
import 'package:supabase_typegen/supabase_typegen.dart';

final _argParser = ArgParser()
..addOption(
'url',
help:
'The Supabase project URL, for example https://xyz.supabase.co. '
'Falls back to the SUPABASE_URL environment variable.',
)
..addOption(
'key',
help:
'The API key used to read the schema description. Falls back to '
'the SUPABASE_ANON_KEY or SUPABASE_KEY environment variable.',
)
..addOption(
'schema',
defaultsTo: 'public',
help: 'The database schema to generate types for.',
)
..addOption(
'output',
abbr: 'o',
defaultsTo: 'lib/supabase_schema.g.dart',
help: 'Path of the generated Dart file.',
)
..addOption(
'import',
defaultsTo: 'package:postgrest/postgrest.dart',
help:
'The import the generated file uses for PostgrestTable and '
'TableColumn.',
)
..addFlag('help', abbr: 'h', negatable: false, help: 'Show this usage.');

Future<void> main(List<String> arguments) async {
// The value returned from main is ignored by the Dart VM, so the exit
// code has to be set explicitly.
exitCode = await _run(arguments);
}

Future<int> _run(List<String> arguments) async {
final ArgResults options;
try {
options = _argParser.parse(arguments);
} on FormatException catch (error) {
stderr
..writeln(error.message)
..writeln(_argParser.usage);
return 64;
}

if (options.flag('help')) {
stdout
..writeln('Generates typed Supabase table definitions from a schema.')
..writeln()
..writeln('Usage: dart run supabase_typegen [options]')
..writeln(_argParser.usage);
return 0;
}

final url = options.option('url') ?? Platform.environment['SUPABASE_URL'];
final key =
options.option('key') ??
Platform.environment['SUPABASE_ANON_KEY'] ??
Platform.environment['SUPABASE_KEY'];
if (url == null || key == null) {
stderr.writeln(
'Both --url and --key are required, either as options or through the '
'SUPABASE_URL and SUPABASE_ANON_KEY environment variables.',
);
return 64;
}

final schemaName = options.option('schema')!;
final baseUrl = url.replaceAll(RegExp(r'/+$'), '');
final endpoint = Uri.parse('$baseUrl/rest/v1/');
final http.Response response;
try {
response = await http.get(
endpoint,
headers: {
'apikey': key,
'Authorization': 'Bearer $key',
'Accept-Profile': schemaName,
},
);
} on http.ClientException catch (error) {
stderr.writeln('Failed to reach $endpoint: $error');
return 1;
}
if (response.statusCode != 200) {
stderr.writeln(
'Failed to fetch the schema description from $endpoint '
'(HTTP ${response.statusCode}): ${response.body}',
);
return 1;
}

final Map<String, dynamic> document;
try {
document =
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
} on FormatException catch (error) {
stderr.writeln('The response from $endpoint is not valid JSON: $error');
return 1;
} on TypeError {
stderr.writeln(
'The response from $endpoint is not an OpenAPI document. Check that '
'the URL points to a Supabase project or PostgREST instance.',
);
return 1;
}

final schema = parseOpenApiDocument(document, schemaName: schemaName);
final code = generateDartCode(schema, importUri: options.option('import')!);

final outputFile = File(options.option('output')!);
outputFile.parent.createSync(recursive: true);
outputFile.writeAsStringSync(code);

final emittedTables = schema.tables
.where((table) => table.columns.isNotEmpty)
.length;
final skippedTables = schema.tables.length - emittedTables;
stdout.writeln(
'Generated ${outputFile.path} with $emittedTables tables and '
'${schema.enums.length} enums from schema "$schemaName".'
'${skippedTables == 0 ? '' : ' Skipped $skippedTables tables '
'without columns.'}',
);
return 0;
}
Loading
Loading