Skip to content

Typesafe queries in Supabase Flutter! Generate Flutter / Dart 🎯 classes from your Supabase schema.

License

Notifications You must be signed in to change notification settings

mmvergara/supadart

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Pub Version Pub Points GitHub Stars Runtime Test GitHub License

Supadart 🎯

Typesafe Supabase Flutter Queries
Generate Flutter / Dart 🎯 classes from your Supabase schema.

// allBooks is a typeof List<Books>
final allBooks = await supabase
      .books
      .select("*")
      .withConverter(Books.converter);

Table of Contents πŸ“š

Features πŸš€

  • 🌐 Cli and Web App
  • πŸ› οΈ Typesafe Queries (Create, Read, Equality)
  • 🧱 Immutable Generated Classes
  • πŸ—‚οΈ Roundtrip Serialization fromJson to toJson and back
  • πŸ“Š Supports Column Selection Queries
  • πŸ”’ Supports all Supabase Major datatypes
  • πŸ—‚οΈ Supports Defined as array types
  • πŸ—‚οΈ Supports Enums

Conversion Table πŸ“Š

Supabase Identifier PostgreSQL Format JSON Type Dart Type Runtime Tested
# int2 smallint integer int type βœ… type[]βœ…
# int4 integer integer int type βœ… type[]βœ…
# int8 bigint integer BigInt type βœ… type[]βœ…
# float4 real number double type βœ… type[]βœ…
# float8 double precision number double type βœ… type[]βœ…
# numeric numeric number num type βœ… type[]βœ…
{} json json object Map<String, dynamic> type βœ… type[]βœ…
{} jsonb jsonb object Map<String, dynamic> type βœ… type[]βœ…
T text text string String type βœ… type[]βœ…
T varchar character varying string String type βœ… type[]βœ…
T uuid uuid string String type βœ… type[]βœ…
πŸ—“οΈ date date string DateTime type βœ… type[]βœ…
πŸ—“οΈ time time without time zone string DateTime type βœ… type[]βœ…
πŸ—“οΈ timetz time with time zone string DateTime type βœ… type[]βœ…
πŸ—“οΈ timestamp timestamp without time zone string DateTime type βœ… type[]βœ…
πŸ—“οΈ timestamptz timestamp with time zone string DateTime type βœ… type[]βœ…
πŸ•’ interval interval string Duration type βœ… type[]βœ…
πŸ’‘ bool boolean boolean bool type βœ… type[]βœ…
πŸ—‚οΈ ENUMS ENUM string Enum type βœ… type[]βœ…

Other Types

Generating Dart Classes

1. Pre-requisites

1.2 Do you have serial types?

if you have serial types you need to add a [supadart:serial] to the column like this

COMMENT ON COLUMN test_table.bigserialx IS '[supadart:serial]';
COMMENT ON COLUMN test_table.smallserialx IS 'you can still add comment [supadart:serial]';
COMMENT ON COLUMN test_table.serialx IS 'this part [supadart:serial] just needs to be included';
-- otherwise the insert method will always ask for a value even though serial types are auto-generated

serial types in general are not available in supabase table editor afaik, so if you did not add them manually via sql editor you probably dont have them. Why do we need this?

1.3 Install Internationalization package

# This is an official package from dart and is used for parsing dates
flutter pub add intl
# or
dart pub add intl

Unless you are not using any date types, you can skip this step

1.4 Use snake casing for table names and column names (Optional)

this tool will automatically convert snake_case to camelCase for both table (GeneratedClassName) and column (FieldName) generated names.

snake_case  => camelCase
user_table  => UserTable
snake_case  => camelCase
user_id     => userId

2. Generate Dart Classes

Using the Web App

Using the Dart CLI

Installation

# 🎯 Active from pub.dev
dart pub global activate supadart

# πŸš€ Run via
supadart
# or
dart pub global run supadart

Quick Start

supadart -u <your-supabase-url> -k <your-supabase-anon-key>
# and you are good to go!

CLI Usage

-h, --help         Show usage information
-i, --init         Initialize config file supadart.yaml
-c, --config       Path to config file of yaml         --(default: supadart.yaml)
-u, --url          Supabase URL                        --(default: supadart.yaml supabase_url)
-k, --key          Supabase ANON KEY                   --(default: supadart.yaml supabase_anon_key)
-o, --output       Output file path, add ./ prefix     --(default: ./lib/generated_classes.dart or ./lib/models/ if --separated is enabled)
-d, --dart         Generation for pure Dart project    --(default: false)
-s, --separated    Separated files for each classes    --(default: false)
-e, --exclude      Select methods to exclude ex.  "toJson,copyWith"
-v, --version

With configuration (Recommended)

Alternatively, you can use a configuration file so you just have to run supadart without any arguments.

Run supadart --init to create a supadart.yaml file in your project root directory.

# supadart.yaml

# Required (if you dont have `-u` specified)
supabase_url: https://xxx.supabase.co
# Required (if you dont have `-k` specified)
supabase_anon_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Optional, where to place the generated classes files default: ./lib/models/
output: lib/models/
# Set to true, if you want to generate separated files for each classes
separated: false
# Set to true, if you are not using Flutter, just normal Dart project
dart: false
# Optional, used to map table names to class names(case-sensitive)
mappings:
  # books: book
  # categories: category
  # children: child
  # people: person

# Optional, used to exclude methods from generated classes
exclude:
  # - toJson
  # - copyWith

then you can just run supadart in the terminal!

# Set the supabase_url and supabase_anon_key in your supadart.yaml file
supadart

# If you dont have the Supabase URL and ANON KEY specified in your .yaml file
supadart -u <your-supabase-url> -k <your-supabase-anon-key>

# If you have a .yaml file in a different location
supadart -c path/to/.yaml

Example Usage

Assuming the following table schema

create table
  public.books (
    id bigint generated by default as identity,
    name character varying not null,
    description text null,
    price integer not null,
    created_at timestamp with time zone not null default now(),
    constraint books_pkey primary key (id)
  ) tablespace pg_default;

1. Use the CLI or the Web App to generate dart classes

class Books implements SupadartClass<Books> {
  final BigInt id;
  final String name;
  final String? description;
  final int price;
  final DateTime? createdAt;

  const Books({
    required this.id,
    required this.name,
    this.description,
    required this.price,
    this.createdAt,
  });

  static String get table_name => 'books';
  static String get c_id => 'id';
  static String get c_name => 'name';
  static String get c_description => 'description';
  static String get c_price => 'price';
  static String get c_createdAt => 'created_at';

  static List<Books> converter(List<Map<String, dynamic>> data) {
    return data.map(Books.fromJson).toList();
  }

  static Books converterSingle(Map<String, dynamic> data) {
    return Books.fromJson(data);
  }

  static Map<String, dynamic> _generateMap({
    BigInt? id,
    String? name,
    String? description,
    int? price,
    DateTime? createdAt,
  }) {
    return {
      if (id != null) 'id': id.toString(),
      if (name != null) 'name': name.toString(),
      if (description != null) 'description': description.toString(),
      if (price != null) 'price': price.toString(),
      if (createdAt != null) 'created_at': createdAt.toUtc().toString(),
    };
  }

  static Map<String, dynamic> insert({
    BigInt? id,
    required String name,
    String? description,
    required int price,
    DateTime? createdAt,
  }) {
    return _generateMap(
      id: id,
      name: name,
      description: description,
      price: price,
      createdAt: createdAt,
    );
  }

  static Map<String, dynamic> update({
    BigInt? id,
    String? name,
    String? description,
    int? price,
    DateTime? createdAt,
  }) {
    return _generateMap(
      id: id,
      name: name,
      description: description,
      price: price,
      createdAt: createdAt,
    );
  }

  factory Books.fromJson(Map<String, dynamic> json) {
    return Books(
      id: json['id'] != null
          ? BigInt.parse(json['id'].toString())
          : BigInt.from(0),
      name: json['name'] != null ? json['name'].toString() : '',
      description:
          json['description'] != null ? json['description'].toString() : '',
      price: json['price'] != null ? json['price'] as int : 0,
      createdAt: json['created_at'] != null
          ? DateTime.tryParse(json['created_at'].toString()) as DateTime
          : DateTime.fromMillisecondsSinceEpoch(0),
    );
  }

  Map<String, dynamic> toJson() {
    // Promotion doesn't work well with public fields due to the possibility of the field being modified elsewhere.
    return _generateMap(
      id: id,
      name: name,
      description: description,
      price: price,
      createdAt: createdAt,
    );
  }
}

2. Using the generated class

we now have a typesafe'ish to interact with the database.

Fetch Data

// allBooks is a typeof List<Books>
final allBooks = await supabase
      .books
      .select("*")
      .withConverter(Books.converter);

Fetch Single Data

// book is a typeof Books
final book = await supabase
      .books
      .select("*")
      .eq(Books.c_id, 1)
      .single()
      .withConverter(Books.converterSingle);

Insert Data

// Yes we know which one's are optional or required.
final data = Books.insert(
  name: 'Learn Flutter',
  description: 'Endless brackets and braces',
  price: 2,
);
await supabase.books.insert(data);

Inset Many Data

final many_data = [
  Books.insert(
    name: 'Learn Minecraft',
    description: 'Endless blocks and bricks',
    price: 2,
  ),
  Books.insert(
    name: 'Description is optional',
    created_at: DateTime.now(),
    price: 2,
  ),
];
await supabase.books.insert(many_data);

Update Data

final newData = Books.update(
  name: 'New Book Name',
);
await supabase.books.update(newData).eq(Books.c_id, 1);

Delete Data

await supabase.books.delete().eq(Books.c_id, 1);

Enums CRUD

-- assuming the following schema
CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral', 'excited', 'angry');
CREATE TABLE enum_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    mood mood NOT NULL
);
// enum names are converted to .toUpperCase()
enum MOOD { happy, sad, neutral, excited, angry }
MOOD firstEnumVal = MOOD.angry;
MOOD newEnumVal = MOOD.excited;

// Create
await supabase.enum_types.insert(EnumTypes.insert(
      mood: firstEnumVal,
    ));

await supabase.enum_types
        // Update
        .update(EnumTypes.update(mood: newEnumVal))
        // Equality ⚠️ you need to do manual ⬇️ enum to string conversion
        .eq(EnumTypes.c_mood, firstEnumVal.toString().split(".").last);

// Read
await supabase.enum_types.select().withConverter(EnumTypes.converter);

Column Selection Queries

final book = await supabase
      .from('books')
      .select('${Books.c_id}, ${Books.c_name}')
      .eq(Books.c_id, 69) // Assuming 69 is the id
      .single()
      .withConverter(Books.converterSingle);
print(book.id);           // 69
print(book.name);         // "Supadart"
print(book.description);  // ""
print(book.price);        // 0
print(book.created_at);   // 1970-01-01 00:00:00.000

if a value is an enum, the first value of that enum will be used as the default value

Contributors

GitHub contributors

About

Typesafe queries in Supabase Flutter! Generate Flutter / Dart 🎯 classes from your Supabase schema.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published