supabase_codegen 1.2.0
supabase_codegen: ^1.2.0 copied to clipboard
A codegen library that grabs and generates classes from Supabase tables and enums in Dart.
Supabase Codegen #
Supabase Codegen generates type-safe Dart models from your Supabase tables automatically!
Installation 💻 #
❗ In order to start using Supabase Codegen you must have the Dart SDK installed on your machine.
Add the following to your pubspec.yaml
dependencies:
supabase_codegen: ^1.2.0
✨ Features #
- Automatically generates Dart classes from Supabase tables
- Creates type-safe models with full IDE support
- Supports complex relationships and nested structures
- Generates getters and setters for all fields
📋 Prerequisites #
- Supabase project with tables
- Dart/Flutter development environment
- Environment configuration file (
.env
)
🛠️ Setup #
-
Install the package. See Installation
-
Create a
.env
file at the root of your project with your Supabase credentials. See example.env. -
Create SQL functions in Supabase.
Options:- Copy and run the sql from get_schema_info and get_enum_types in your Supabase project.
- Create migration to apply to your local or remote database with
dart run supabase_codegen:add_codegen_functions
and apply the migration withsupabase migration up
.
Note: this requires Supabase CLI with linked project
-
Run the generation script:
dart run supabase_codegen:generate_types
Command Line Options #
You can customize the type generation process with the following command-line options:
-e, --env <env_file>
(Default: .env):
Specifies the path to the env file containing your Supabase credentials (See example.env).
Example: dart run supabase_codegen:generate_types -e .env.local
-o, --output <output_folder>
(Default: supabase/types):
Sets the directory where the generated type files will be placed.
Example: dart run supabase_codegen:generate_types -o lib/models/supabase
-t, --tag <tag>
(Default: ''):
Adds a tag to the generated files to help with versioning or distinguishing between different schemas.
Example: dart run supabase_codegen:generate_types -t v2
If set, the tag will appear at the end of the files following the file generation timestamp like this
/// Generated by supabase_codegen (1.2.0)
/// Date: 2025-03-06 15:43:24.078502
/// Tag: v2
-c, --config-yaml <config yaml path>
Path to config yaml file, defaults to ".supabase_codegen.yaml".
If not specified, reads from keys under supabase_codegen
in pubspec.yaml
.
See Yaml configuration
-d, --debug
(Default: false):
Enables debug logging to provide more verbose output during the type generation.
Example: dart run supabase_codegen:generate_types -d
-s, --[no-]skip-footer
Skip the writing of the footer in the generated files.
Example: dart run supabase_codegen:generate_types --skip-footer
-h, --help
Show command line usage options
Example: dart run supabase_codegen:generate_types --help
Yaml configuration #
Instead of providing the options via the command line, you can also set them in a yaml file.
This can be either in the config yaml file (default .supabase_codegen.yaml
) or in your pubspec.yaml
file under the supabase_codegen
key.
This allows setting default values, and you only need to override them if needed from the command line.
Example config file e.g. .supabase_codegen.yaml
env: .env.development # Overrides default: .env
output: lib/models/supabase # Overrides default: supabase/types
tag: v1 # Overrides default: ''
debug: true # Overrides default: false
skipFooter: true # Overrides default: false
Here's an example of how to configure the options in pubspec.yaml
:
name: my_supabase_app
description: A sample Supabase app.
dev_dependencies:
supabase_codegen: ^1.2.0
supabase_codegen:
env: .env.development # Overrides default: .env
output: lib/models/supabase # Overrides default: supabase/types
tag: v1 # Overrides default: ''
debug: true # Overrides default: false
skipFooter: true # Overrides default: false
Explanation (See Command Line Options): #
env
: Sets the default path to the env file.
output
: Sets the default output folder.
tag
: Sets the default tag that will be added to the generated files.
debug
: Sets the default for debug logging.
skipFooter
: Skip the writing of the footer in the generated files.
Priority #
The command line options have higher priority than the options defined in the yaml configuration.
Order:
command line options
-> configuration yaml (default: .supabase_codegen.yaml
)
-> pubspec.yaml (key: supabase_codegen
)
📦 Generated Types #
The generator will create strongly-typed models like this:
enum UserRole {
admin,
user,
}
/// Users Table
class UsersTable extends SupabaseTable<UsersRow> {
/// Table Name
@override
String get tableName => 'users';
/// Create a [UsersRow] from the [data] provided
@override
UsersRow createRow(Map<String, dynamic> data) => UsersRow.fromJson(data);
}
/// Users Row
class UsersRow extends SupabaseDataRow {
/// Users Row
UsersRow({
required String email,
required UserRole role,
String? id,
String? accName,
String? phoneNumber,
List<String>? contacts,
DateTime? createdAt,
}) : super({
'email': supaSerialize(email),
'role': supaSerialize(role),
if (id != null) 'id': supaSerialize(id),
if (accName != null) 'acc_name': supaSerialize(accName),
if (phoneNumber != null) 'phone_number': supaSerialize(phoneNumber),
if (contacts != null) 'contacts': supaSerialize(contacts),
if (createdAt != null) 'created_at': supaSerialize(createdAt),
});
/// Users Row
const UsersRow._(super.data);
/// Create Users Row from a [data] map
factory UsersRow.fromJson(Map<String, dynamic> data) =>
UsersRow._(data.cleaned);
/// Get the Json representation of the row
Map<String, dynamic> toJson() => data;
/// Get the [SupabaseTable] for this row
@override
SupabaseTable get table => UsersTable();
/// Id field name
static const String idField = 'id';
/// Id
String get id => getField<String>(idField, defaultValue: '')!;
set id(String value) => setField<String>(idField, value);
/// Email field name
static const String emailField = 'email';
/// Email
String get email => getField<String>(emailField)!;
set email(String value) => setField<String>(emailField, value);
/// Acc Name field name
static const String accNameField = 'acc_name';
/// Acc Name
String? get accName => getField<String>(accNameField);
set accName(String? value) => setField<String>(accNameField, value);
/// Phone Number field name
static const String phoneNumberField = 'phone_number';
/// Phone Number
String? get phoneNumber => getField<String>(phoneNumberField);
set phoneNumber(String? value) => setField<String>(phoneNumberField, value);
/// Contacts field name
static const String contactsField = 'contacts';
/// Contacts
List<String> get contacts => getListField<String>(contactsField);
set contacts(List<String>? value) =>
setListField<String>(contactsField, value);
/// Role field name
static const String roleField = 'role';
/// Role
UserRole get role =>
getField<UserRole>(roleField, enumValues: UserRole.values)!;
set role(UserRole value) => setField<UserRole>(roleField, value);
/// Created At field name
static const String createdAtField = 'created_at';
/// Created At
DateTime get createdAt =>
getField<DateTime>(createdAtField, defaultValue: DateTime.now())!;
set createdAt(DateTime value) => setField<DateTime>(createdAtField, value);
/// Make a copy of the current [UsersRow]
/// overriding the provided fields
UsersRow copyWith({
String? email,
UserRole? role,
String? id,
String? accName,
String? phoneNumber,
List<String>? contacts,
DateTime? createdAt,
}) =>
UsersRow.fromJson({
'email': email ?? data['email'],
'role': role?.name ?? data['role'],
'id': id ?? data['id'],
'acc_name': accName ?? data['acc_name'],
'phone_number': phoneNumber ?? data['phone_number'],
'contacts': contacts ?? data['contacts'],
'created_at': createdAt ?? data['created_at'],
});
}
🚀 Usage Examples #
Creating Records #
final usersTable = UsersTable();
// Create new record
final adminUser = await usersTable.insert({
UsersRow.emailField: 'john@example.com',
UsersRow.roleField: UserRole.admin.name,
UsersRow.accNameField: 'John Doe',
UsersRow.phoneNumberField: '+1234567890',
});
// The returned object is already typed
print(adminUser.email);
print(adminUser.accName);
/// Create new record with row object
final user = UsersRow(
email: 'user@example.com',
role: UserRole.user,
accName: 'Regular User',
contacts: [
adminUser.email,
],
);
await usersTable.insertRow(user);
Reading Data #
final usersTable = UsersTable();
// Fetch a single user
final user = await usersTable.querySingleRow(
queryFn: (q) => q.eq(UsersRow.idField, '123'),
);
// Access typed properties
print(user.email);
print(user.accName);
print(user.phoneNumber);
print(user.createdAt);
// Fetch multiple users
final adminUsers = await usersTable.queryRows(
queryFn: (q) => q
.eq(UsersRow.roleField, UserRole.admin.name)
.order(UserRow.emailField),
);
// Work with typed objects
for (final user in adminUsers) {
print('User ${user.id}:');
print('- Email: ${user.email}');
print('- Name: ${user.accName ?? "No name set"}');
print('- Phone: ${user.phoneNumber ?? "No phone set"}');
print('- Created: ${user.createdAt}');
}
// Query with complex conditions
final recentUsers = await usersTable.queryRows(
queryFn: (q) => q
.gte(UsersRow.createdAtField, DateTime.now().subtract(Duration(days: 7)))
.ilike(UsersRow.emailField, '%@gmail.com')
.order(UsersRow.createdAtField, ascending: false),
);
Updating Records #
final usersTable = UsersTable();
// Update by query (with data)
await usersTable.update(
data: {'acc_name': 'Jane Doe'},
matchingRows: (q) => q.eq('id', '123'),
);
// Update with return value
final updatedUsers = await usersTable.update(
data: {'role': UserRole.admin.name},
matchingRows: (q) => q.in_(UsersRow.idField, ['1', '2', '3']),
returnRows: true,
);
// Update by query (with row)
await usersTable.update(
row: user.copyWith(
contacts: [
...user.contacts,
'some_other_user@example.com',
],
),
matchingRows: (q) => q.eq(UsersRow.idField, user.id),
);
Upserting Records #
final usersTable = UsersTable();
// Upsert with data
final otherAdmin = await usersTable.upsert(
{
UsersRow.idField: '123',
UsersRow.emailField: 'jane@example.com',
UsersRow.roleField: UserRole.admin.name,
UsersRow.accNameField: 'Jane Doe',
},
);
// Upsert with row
final user = await usersTable.querySingleRow(
queryFn: (q) => q.eq(UsersRow.idField, '123'),
);
final updatedUser = await usersTable.upsertRow(
user.copyWith(role: User.admin),
onConflict: '${UsersRow.idField}, ${UsersRow.emailField}',
);
print(updatedUser.role); // UserRole.admin
Deleting Records #
final usersTable = UsersTable();
// Delete single record
await usersTable.delete(
matchingRows: (q) => q.eq(UsersRow.idField, 123),
);
// Delete with return value
final deletedUsers = await usersTable.delete(
matchingRows: (q) => q.eq(UsersRow.roleField, UserRole.user.name),
returnRows: true,
);
Working with Related Data #
// Get a pilot and their documents
final pilotsTable = PilotsTable();
final documentsTable = DocumentsTable();
// Get pilot
final pilots = await pilotsTable.queryRows(
queryFn: (q) => q.eq('id', pilotId),
);
final pilot = pilots.firstOrNull;
// Get related documents
if (pilot != null) {
final documents = await documentsTable.queryRows(
queryFn: (q) => q.eq('pilot_id', pilot.id),
);
}
Testing #
Unit Tests #
To run the unit tests, use the following command:
dart test
Using mocks #
To load a preconfigured mock supabase client for testing run loadMockSupabaseClient()
during setUpAll
of your tests.
The variables mockSupabase
and mockSupabaseHttpClient
are available for use during testing.
See supabase_table_test for an example of this in action.
For further details about these mock clients see MockSupabaseHttpClient.
📝 Notes #
- Ensure your Supabase tables have proper primary keys defined
- All generated models are null-safe
- Custom column types are supported through type converters
🤝 Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
Continuous Integration 🤖 #
Supabase Codegen comes with a built-in GitHub Actions workflow powered by Very Good Workflows but you can also add your preferred CI/CD solution.
Out of the box, on each pull request and push, the CI formats
, lints
, and tests
the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses Very Good Analysis for a strict set of analysis options used by our team. Code coverage is enforced using the Very Good Workflows.
📄 License #
This project is licensed under the GPL-3.0 license - see the LICENSE file for details.
Attributions #
Built using the great work by Kennerd at Supabase Flutter Codegen