Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generated classes mark autogenerated fields as required - primary id keys and timestamps #107

Open
pepie opened this issue Dec 10, 2024 · 18 comments
Assignees
Labels
enhancement New feature or request

Comments

@pepie
Copy link

pepie commented Dec 10, 2024

Environment:
    Supabase:  1.219.2
    Supadart: 1.6.5
    Flutter: 3.24.5 • channel stable 
    Dart: 3.5.4 

Hello,

First, thank you for your work. It saves us a lot of time and effort.

Supadart will mark all required db fields as required in the generated class.
This means fields like the db primary id field and timestamps (which are required and automatically generated) will be required when instantiating an object for that table.

Would it be possible to have a constructor /factory that returns Object with id/date fields populated?

Ex:

Assume this simple Genre table:

create table
  public.genres (
    id uuid not null default extensions.uuid_generate_v4 (),
    name character varying(30) not null,
    created_date timestamp with time zone not null default now(),
    last_updated timestamp with time zone not null default now(),

    constraint genres_pkey primary key (id),
    constraint genres_name_key unique (name)
  ) tablespace pg_default;

The generated code is

class Genre implements SupadartClass<Genre> {
  final String name;
  final DateTime createdDate;
  final DateTime lastUpdated;
  final String id;

  const Genre({
    required this.name,
    required this.createdDate,
    required this.lastUpdated,
    required this.id,
  });

This means Genre needs to be instantiated with an ID and a value for create/update date.

final Genre = Genre(
              id: <id>, 
              name: <name>, 
             createdDate: <date>,
             lastUpdated: <data>
 );

The expectation is to use

 final Genre genre = Genre(name: 'genre 1');

I can see from the docs that Object.fromJson() is available, but that means I would have to instantiate objects like

final Genre genre = Genre.fromJson({
        "name": "Genre 1"
  })

The name option is Object.insert() which returns a map. So we're looking at something like

final Genre genre  = Genre.fromJson(Genre.insert(name: 'genre 1'))

Both of these approaches seems verbose and cumbersome.

Here is my supadart.yaml:

 supabase_url: <url here>
 supabase_anon_key:  <hey here>
 output: lib/models/
 separated: true
dart: false

@mmvergara mmvergara self-assigned this Dec 10, 2024
@mmvergara
Copy link
Owner

Hi @pepie , thanks for opening an issue!

So just to be clear the goal is to instantiate a Genre object with default values.

Im thinking of adding a factory method called "New" to instantiate it with default values (it will be optional, configurable in the supadart.yaml) .


So the real question is what are the default values, im thinking of using dartTypeDefaultNullValue() function that the from_json uses.

But the thing about that function is its not "postgres default value" if you know what i mean.
for ex.
uuid default value
will just be ""
instead of
"00000000-0000-0000-0000-000000000000"

I think it'll be fine in the client runtime but when you try to push that object into the database it probably wont work until you change those values.


I'd like your thoughts on this, ill work on it as soon as i get your feedback

class Test {
  final String name;
  final int age;

  Test({required this.name, required this.age});

  factory Test.New({String name = 'John', int age = 30}) {
    return Test(name: name, age: age);
  }
}

void main() {
  var test1 = Test.New(); // Uses default values
  print('Name: ${test1.name}, Age: ${test1.age}');

  var test2 = Test.New(name: 'Alice'); // Uses default age
  print('Name: ${test2.name}, Age: ${test2.age}');

  var test3 = Test.New(age: 25); // Uses default name
  print('Name: ${test3.name}, Age: ${test3.age}');
}

// Output
// Name: John, Age: 30
// Name: Alice, Age: 30
// Name: John, Age: 25

@pepie
Copy link
Author

pepie commented Dec 12, 2024

Thanks @mmvergara ,

I haven’t saved anything with this package yet but plan to today.

For a table defined as

create table
  public.genres (
    id uuid not null default extensions.uuid_generate_v4 (),
    name character varying(30) not null,
    created_date timestamp with time zone not null default now(),
    last_updated timestamp with time zone not null default now(),
  );

I can instantiate the generated class using T.fromJson() and T.insert() to avoid having to initialize the id and and timestamp fields - which are supposed to be generated by the server:

Genre g = Genre.fromJson( Genre.insert( 
    name: "test one"
))

It would be nice to have a simpler option like:

Genre g = Genre.new ( 
    name: "test one"
)

I think most of the work is already done in T.insert() or _generateMap(). The only change needed is to return an object instead of a map:

class <T> implements SupadartClass<Genre> {
  final String name;
  final DateTime createdDate;
  final DateTime lastUpdated;
  final String id;
 ...
 }

These two functions already have context on which fields are optional, it need only return the object instead of a json map.

static <T>  _generateMap({
    String? name,
    DateTime? createdDate,
    DateTime? lastUpdated,
    String? id,
  }) {
    return {
      if (name != null) 'name': name,
      if (createdDate != null)
        'created_date': createdDate.toUtc().toIso8601String(),
      if (lastUpdated != null)
        'last_updated': lastUpdated.toUtc().toIso8601String(),
      if (id != null) 'id': id,
    };
  }

To avoid guessing default values, I’d suggest leaving optional fields uninitialized and letting the database handle defaults on insert.

Sending null or an empty value for UUID/timestamp fields should work, as the server will populate defaults on insert.

So, to answer your question above, sending null or ' ' for the uuid / timestamp fields should be fine.

Since _generateMap() and T.insert() already know which fields are optional, consider replicating _generateMap() as T.new() and return an object instead of a JSON map:

static <T> new({
    String? name,
    DateTime? createdDate,
    DateTime? lastUpdated,
    String? id,
  }) {
    return {
      if (name != null) 'name': name,
      if (createdDate != null)    'created_date': createdDate,
      if (lastUpdated != null)    'last_updated': lastUpdated,
      if (id != null) 'id': id,
    };
  }

@mmvergara
Copy link
Owner

This is sample generation implementing the new method (is renamed to New because we can use the word new) from

class StringTypes implements SupadartClass<StringTypes> {
  final String id;
  final String? colUuid;
  final List<String>? colUuidArray;
  final String? colCharacter;
  final List<String>? colCharacterArray;
  final String? colCharactervarying;
  final List<String>? colCharactervaryingArray;
  final String? colText;
  final List<String>? colTextArray;

  const StringTypes({
    required this.id,
    this.colUuid,
    this.colUuidArray,
    this.colCharacter,
    this.colCharacterArray,
    this.colCharactervarying,
    this.colCharactervaryingArray,
    this.colText,
    this.colTextArray,
  });

  static String get table_name => 'string_types';
  static String get c_id => 'id';
  static String get c_colUuid => 'col_uuid';
  static String get c_colUuidArray => 'col_uuid_array';
  static String get c_colCharacter => 'col_character';
  static String get c_colCharacterArray => 'col_character_array';
  static String get c_colCharactervarying => 'col_charactervarying';
  static String get c_colCharactervaryingArray => 'col_charactervarying_array';
  static String get c_colText => 'col_text';
  static String get c_colTextArray => 'col_text_array';

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

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

  static Map<String, dynamic> _generateMap({
    String? id,
    String? colUuid,
    List<String>? colUuidArray,
    String? colCharacter,
    List<String>? colCharacterArray,
    String? colCharactervarying,
    List<String>? colCharactervaryingArray,
    String? colText,
    List<String>? colTextArray,
  }) {
    return {
      if (id != null) 'id': id,
      if (colUuid != null) 'col_uuid': colUuid,
      if (colUuidArray != null)
        'col_uuid_array': colUuidArray.map((e) => e).toList(),
      if (colCharacter != null) 'col_character': colCharacter,
      if (colCharacterArray != null)
        'col_character_array': colCharacterArray.map((e) => e).toList(),
      if (colCharactervarying != null)
        'col_charactervarying': colCharactervarying,
      if (colCharactervaryingArray != null)
        'col_charactervarying_array':
            colCharactervaryingArray.map((e) => e).toList(),
      if (colText != null) 'col_text': colText,
      if (colTextArray != null)
        'col_text_array': colTextArray.map((e) => e).toList(),
    };
  }
   // New Method
   // New Method
   // New Method
  static Object New({
    String? id,
    String? colUuid,
    List<String>? colUuidArray,
    String? colCharacter,
    List<String>? colCharacterArray,
    String? colCharactervarying,
    List<String>? colCharactervaryingArray,
    String? colText,
    List<String>? colTextArray,
  }) {
    return {
      if (id != null) 'id': id,
      if (colUuid != null) 'col_uuid': colUuid,
      if (colUuidArray != null) 'col_uuid_array': colUuidArray,
      if (colCharacter != null) 'col_character': colCharacter,
      if (colCharacterArray != null) 'col_character_array': colCharacterArray,
      if (colCharactervarying != null)
        'col_charactervarying': colCharactervarying,
      if (colCharactervaryingArray != null)
        'col_charactervarying_array': colCharactervaryingArray,
      if (colText != null) 'col_text': colText,
      if (colTextArray != null) 'col_text_array': colTextArray,
    };
  }

  static Map<String, dynamic> insert({
    String? id,
    String? colUuid,
    List<String>? colUuidArray,
    String? colCharacter,
    List<String>? colCharacterArray,
    String? colCharactervarying,
    List<String>? colCharactervaryingArray,
    String? colText,
    List<String>? colTextArray,
  }) {
    return _generateMap(
      id: id,
      colUuid: colUuid,
      colUuidArray: colUuidArray,
      colCharacter: colCharacter,
      colCharacterArray: colCharacterArray,
      colCharactervarying: colCharactervarying,
      colCharactervaryingArray: colCharactervaryingArray,
      colText: colText,
      colTextArray: colTextArray,
    );
  }

  static Map<String, dynamic> update({
    String? id,
    String? colUuid,
    List<String>? colUuidArray,
    String? colCharacter,
    List<String>? colCharacterArray,
    String? colCharactervarying,
    List<String>? colCharactervaryingArray,
    String? colText,
    List<String>? colTextArray,
  }) {
    return _generateMap(
      id: id,
      colUuid: colUuid,
      colUuidArray: colUuidArray,
      colCharacter: colCharacter,
      colCharacterArray: colCharacterArray,
      colCharactervarying: colCharactervarying,
      colCharactervaryingArray: colCharactervaryingArray,
      colText: colText,
      colTextArray: colTextArray,
    );
  }

  factory StringTypes.fromJson(Map<String, dynamic> jsonn) {
    return StringTypes(
      id: jsonn['id'] != null ? jsonn['id'].toString() : '',
      colUuid: jsonn['col_uuid'] != null ? jsonn['col_uuid'].toString() : '',
      colUuidArray: jsonn['col_uuid_array'] != null
          ? (jsonn['col_uuid_array'] as List<dynamic>)
              .map((v) => v.toString())
              .toList()
          : <String>[],
      colCharacter: jsonn['col_character'] != null
          ? jsonn['col_character'].toString()
          : '',
      colCharacterArray: jsonn['col_character_array'] != null
          ? (jsonn['col_character_array'] as List<dynamic>)
              .map((v) => v.toString())
              .toList()
          : <String>[],
      colCharactervarying: jsonn['col_charactervarying'] != null
          ? jsonn['col_charactervarying'].toString()
          : '',
      colCharactervaryingArray: jsonn['col_charactervarying_array'] != null
          ? (jsonn['col_charactervarying_array'] as List<dynamic>)
              .map((v) => v.toString())
              .toList()
          : <String>[],
      colText: jsonn['col_text'] != null ? jsonn['col_text'].toString() : '',
      colTextArray: jsonn['col_text_array'] != null
          ? (jsonn['col_text_array'] as List<dynamic>)
              .map((v) => v.toString())
              .toList()
          : <String>[],
    );
  }

  Map<String, dynamic> toJson() {
    return _generateMap(
      id: id,
      colUuid: colUuid,
      colUuidArray: colUuidArray,
      colCharacter: colCharacter,
      colCharacterArray: colCharacterArray,
      colCharactervarying: colCharactervarying,
      colCharactervaryingArray: colCharactervaryingArray,
      colText: colText,
      colTextArray: colTextArray,
    );
  }

  StringTypes copyWith({
    String? id,
    String? colUuid,
    List<String>? colUuidArray,
    String? colCharacter,
    List<String>? colCharacterArray,
    String? colCharactervarying,
    List<String>? colCharactervaryingArray,
    String? colText,
    List<String>? colTextArray,
  }) {
    return StringTypes(
      id: id ?? this.id,
      colUuid: colUuid ?? this.colUuid,
      colUuidArray: colUuidArray ?? this.colUuidArray,
      colCharacter: colCharacter ?? this.colCharacter,
      colCharacterArray: colCharacterArray ?? this.colCharacterArray,
      colCharactervarying: colCharactervarying ?? this.colCharactervarying,
      colCharactervaryingArray:
          colCharactervaryingArray ?? this.colCharactervaryingArray,
      colText: colText ?? this.colText,
      colTextArray: colTextArray ?? this.colTextArray,
    );
  }
}

does it seem good to you?

@pepie
Copy link
Author

pepie commented Dec 12, 2024

That looks good.
Let's try it out.

@mmvergara
Copy link
Owner

updated to 1.6.8 you can try it out now

@pepie
Copy link
Author

pepie commented Dec 13, 2024

I did a quick test on v 1.6.8 and it looks good.
The only thing I noticed is having to cast the return object to , since new T.New() returns an "Object".

It would be nice to have it return the class, similar the .fromJson() factory method.

He's the updated usage example:

OLD:

Genre g =  Genre.fromJson( Genre.insert(
    name: 'some name'
));

NEW:

Genre g = Genre.New(  name ) as Genre;

@wisewinshin
Copy link

The TypeScript code generated using the Supabase CLI wraps the existing types with an Insert type, allowing you to omit auto-generated values. Would it be possible to use that approach?

@mmvergara
Copy link
Owner

The TypeScript code generated using the Supabase CLI wraps the existing types with an Insert type, allowing you to omit auto-generated values. Would it be possible to use that approach?

do you mean like this?

factory StringTypes.fromJson(Map<String, dynamic> jsonn) {
    return StringTypes(
      id: jsonn['id'] != null ? jsonn['id'].toString() : '',
      ...
   )

sorry i don't quite follow.

@mmvergara
Copy link
Owner

mmvergara commented Dec 13, 2024

NEW:

Genre g = Genre.New(  name ) as Genre;

@pepie I see, i should be able to change it easily. rn it returns it as an Object so i should change it to return as Genre type.

By the way, are you trying to create the class instance locally and send it to the server, letting the server populate those values? I'm not entirely sure about the use case for this new method, especially since we already have the insert method for that purpose.

@pepie
Copy link
Author

pepie commented Dec 14, 2024

Yes.

Here's a sample use case:

Genre genre = Genre.new(
    name: 'Classical'
);

Genre g = await _genreService.create ( genre );
print(g);

Output:

{
  id: 'abc123',
  name: 'Classical',
  createdDated: '202-12-01...',
  lastUpdated: '202-12-01...',
}

Create function:

Future<Genre?> create(Genre genre) async{
    try {

      return  await supabase
          .genres
          .insert( genre.toJson() )
          .single();

    } catch( error, trace){
      log( "create failed ",  error: error,  stackTrace: trace );
      ...
    }
  }

Instantiating Genre locally helps me mocking or manipulating the object before sending it back to the server.
ex:

ex:
List<Genre> = List.generate(
   10, 
   ( int index ) => Genre.New( name: "item $index" )
).toList();

.insert() works great but it returns a JSON map. An object is easier to work with and provides better type safety.
You can simply duplicate .insert and have it return object instead of a map.

I am wondering if you need .insert() to return a JSON at all, since you can get that with .toJson().
I can see the current implementation being useful in a web dev environment (like Node/Typescript), where json is a first-class citizen. But you will most often work with objects and use the dot pattern to access fields/methods.
I would only need a JSON in specific cases, and can get it with the following snippet when needed

Map json =  Object.New(...).toJson();

I think .insert() is a nice wrapper, but it doesn't completely replace the need for a constructor/function that returns the object.

@mmvergara
Copy link
Owner

I see, I didn't think about that.


I am wondering if you need .insert() to return a JSON at all, since you can get that with .toJson().

it needs to return a JSON because assuming we do insert then only the required fields, then if we cast it to the Class it will fail because some required (non-nullable in the database) fields are there like the field id. It will throw an error

So trying out the new method which is

  static Object New({
    String? id,
    String? colUuid,
    List<String>? colUuidArray,
    String? colCharacter,
    List<String>? colCharacterArray,
    String? colCharactervarying,
    List<String>? colCharactervaryingArray,
    String? colText,
    List<String>? colTextArray,
  }) {
    return {
      if (id != null) 'id': id,
      if (colUuid != null) 'col_uuid': colUuid,
      if (colUuidArray != null) 'col_uuid_array': colUuidArray,
      if (colCharacter != null) 'col_character': colCharacter,
      if (colCharacterArray != null) 'col_character_array': colCharacterArray,
      if (colCharactervarying != null)
        'col_charactervarying': colCharactervarying,
      if (colCharactervaryingArray != null)
        'col_charactervarying_array': colCharactervaryingArray,
      if (colText != null) 'col_text': colText,
      if (colTextArray != null) 'col_text_array': colTextArray,
    };
  }

Then doing

StringTypes g = StringTypes.New() as StringTypes;

Will throw this error

TypeError: Instance of 'IdentityMap<String, Object>': type 'IdentityMap<String, Object>' is not a
subtype of type 'StringTypes'

So i just realized the New method won't work


Best thing i could do is probably this

  static StringTypes New({
    String? id,
    String? colUuid,
    List<String>? colUuidArray,
    String? colCharacter,
    List<String>? colCharacterArray,
    String? colCharactervarying,
    List<String>? colCharactervaryingArray,
    String? colText,
    List<String>? colTextArray,
  }) {
    return StringTypes.fromJson(StringTypes.insert(
      id: id,
      colUuid: colUuid,
      colUuidArray: colUuidArray,
      colCharacter: colCharacter,
      colCharacterArray: colCharacterArray,
      colCharactervarying: colCharactervarying,
      colCharactervaryingArray: colCharactervaryingArray,
      colText: colText,
    ));
  }

which is just basically what you described last time

Genre g =  Genre.fromJson( Genre.insert(
    name: 'some name'
));

but this populates the properties with default values, which is against on what you want which is to instantiate the class, then have the server populate it. am i understanding it correctly? i maybe out of it again lol

@pepie
Copy link
Author

pepie commented Dec 14, 2024

Yes, that's correct.

I imaging populating the properties with default values would be difficult to maintain.

This could be problematic in the case where we have a timestamp or an auto-incremented column, since the values would not be generated by the DB server.

I'll try to complete a full insert today and let you know if what we have so far doesn't work.

Some additional thoughts:

Optional fields are null by default, so is it possible to simply not initialize them? And leave it to the developer to initialize them when needed?

This would add the flexibility needed, I think.

Imagine the following table, with auto-generated columns and one nullable column with no default value

create table
  public.genres (

    name character varying(30) not null,        <-- required & not generated => required in class
    nickname character varying(30)  null,     <-- nullable &  not generated => optional in class

    -- required but auto-generated => optional in class

    id uuid not null default extensions.uuid_generate_v4 (),   
    created_date timestamp with time zone not null default now(),  
    last_updated timestamp with time zone not null default now(),  
  )

This should produce a class like

class Genre {
   String name;      <- only required field, everything else is optional

   String? id;
   String? color;
   DateTime? createdDate;
   DateTime? lastUpdated;

    ....
}

Instantiating with

Genre genre = Genre.new(
    name: 'Classical'
);

Should create an object with the following values

{
  name: 'Classical',

  id: null,
  color: null,
  createdDated: null,
  lastUpdated: null,

}

Now, I can work with Genre locally until I am ready to send it to the server with

Genre? g = await _genreService.create ( 
     genre.copyWith( color: 'red' )                
);
print( g );

The server would receive the following values

{
  name: 'Classical',
  color: 'red,

  id: null,
  createdDated: null,
  lastUpdated: null

}

and would apply the relevant rules/constraints to the columns with null values, and return

{
  id: 'abc123xyz',
  name: 'Classical',
  color: 'red,
  createdDated: '2024-12-01....',
  lastUpdated: '2024-12-01....',
}

The takeaway here, is we delegate optional field initialization to the developer.
We leave all optional fields alone, and assume the developer have enough context about the model to make adjustments as needed.

For example, I can initialize the option color field like:

Genere genre = Genre.New( name: 'classical' );

// adjustments

g.color = 'red'; 

or

final g = genre.copyWith( color='red');

// or take care of all defauls when I create the object like

Genre genre = Genre.new(
    id: generateNewKeyFunction(),
    name: 'Classical', 
    createdDate: DateTime.now()
);

//save
await _createGenre ( g );

@mmvergara
Copy link
Owner

mmvergara commented Dec 14, 2024

so altering the class to be

create table
  public.genres (

    name character varying(30) not null,        <-- required & not generated => required in class
    nickname character varying(30)  null,     <-- nullable &  not generated => optional in class

    -- required but auto-generated => optional in class

    id uuid not null default extensions.uuid_generate_v4 (),   
    created_date timestamp with time zone not null default now(),  
    last_updated timestamp with time zone not null default now(),  
  )
class Genre {
   String name;      <- only required field, everything else is optional

   String? id;
   String? color;
   DateTime? createdDate;
   DateTime? lastUpdated;

    ....
}

But the rule of the class right now is

  • attribute can be null if it can be null in the server
  • attribute is required it can't be null in the server

What you are proposing is changing this and do

  • attribute can be null if it can be inserted as null in the server
  • attribute is required if it can't be inserted in the server as null

am i following?

I don't know how i feel about leaving database not nullable fields like ID in the class because in the supabase typescript that's not how it works.

I can do some tweaking for you and have an option to apply the rule you are proposing. so you can just instantiate the class directly from the constructor. just let me know.

another thought i have is that leaving everything as can be 'null' to allow for select columns without leaving the other fields value as default_value, this got me thinking now

@pepie
Copy link
Author

pepie commented Dec 14, 2024

Yeah,
I think we might be overthinking this.

At the end of the day, I just want to instantiate the class without having to specify the timestamp field or anything that will be generated by the server.

So far I've been able to do that with

T object = T.fromJson(T.insert(...));

The .new() addition works fine too, so far.
Let me play with it and see if more is needed. I wouldn't want you do unnecessary tweaks to the code.

I'll have more information by tonight.

@wisewinshin
Copy link

The TypeScript code generated using the Supabase CLI wraps the existing types with an Insert type, allowing you to omit auto-generated values. Would it be possible to use that approach?

do you mean like this?

factory StringTypes.fromJson(Map<String, dynamic> jsonn) {
    return StringTypes(
      id: jsonn['id'] != null ? jsonn['id'].toString() : '',
      ...
   )

sorry i don't quite follow.

The types generated based on the Supabase CLI are typically defined as Tables<'table_name'> when retrieving objects from a table. However, when inserting new objects into a table, they are declared using TablesInsert<'table_name'>. Would it be challenging to distinguish between auto-generated values in this manner?

I have some experience with TypeScript but understand that it differs from Dart classes. I think it might be difficult to find an alternative as well.

@mmvergara
Copy link
Owner

The types generated based on the Supabase CLI are typically defined as Tables<'table_name'> when retrieving objects from a table. However, when inserting new objects into a table, they are declared using TablesInsert<'table_name'>. Would it be challenging to distinguish between auto-generated values in this manner?

I have some experience with TypeScript but understand that it differs from Dart classes. I think it might be difficult to find an alternative as well.

@wisewinshin

The generated class .insert() method does not require you to provide values for fields that are auto-generated or have default values on the schema/database.
for ex.

books_table
id - is auto generated
name - string
created_at - has a default value of now

you can just do

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

and it will return

{ "name":"your_name" }

which then you can use the supabase sdk to insert,

await supabase.books.insert(data);

are you talking about the .insert() method of the sdk itself

@pepie
Copy link
Author

pepie commented Dec 26, 2024

Hey,

I finally got a chance to test inserts and have few updates.

To answer your question, I was referring to T.insert() in my previous post - the insert statement generated with the class, not .insert() from the sdk ( supabase..insert() ).

I noticed today the object created with T.New() will mark ALL fields as optional. I missed that from your previous message , but I ended up going back to using T.fromJson( T.insert(...)) so I can leverage the null safety features.

Ex: Given a Genre table with one a required 'name' column:

create table
  public.genres (
     id uuid not null default extensions.uuid_generate_v4 (),
    name character varying(30) not null,

  ) 

using

Genre.fromJson( 
    Genre.insert( 
        name: 'Test'
    )
)```

 will correctly create  the class as

class Genre {
String? id; <-- optional
String name; <-- required
...
}

Genre.New( name: 'Test' ) , however, will produce

class {
String? id;
String? name; <-- should not be optional
...
}

  
Inserts will  also fail for records with an array column, if the class is created with T.New()
Error: "malformed array literal" 

My table is defined as

create table
public.events (
id uuid not null default extensions.uuid_generate_v4 (),
title character varying(100) not null,
category character varying[] not null <-- this causes "malformed array literal"
...
)


This is the only way I could get inserts to work:

final event = Event.fromJson(Event.insert(...));
await create(event);
...

Future<Event?> create(Event event) async{

try {

  Map<String, dynamic> record = event.toJson();    <-- use/convert to JSON so array columns work
  
  record.remove("id");     <-- manually remove id field so Postgres don't complain


  final result = await supabase
      .events
      .insert(record)
      .select("*");          <--- hack: using .single() will fail
      //.withConverter(Event.converterSingle)        <-- hack: using .single() with Event.convertSingle() fails.

  return Event.fromJson(            <-- needed since we can't use  .single() and .withConverter( <T>.converterSingle() ) fails 
         result.single
  );

} on PostgrestException catch(error, trace){
     ...
} 
...

}


Note:
.withConverter( T.convertSingle) only fails for inserts. It works find for other queries.

@mmvergara
Copy link
Owner

Things are a bit out of place


Regarding the T.new()

I think we should just give up this feature, it's too complicated and can even cause runtime issues in your app,

If this pattern works well for you i think we should just keep it that way, unless there are more problems

Genres.fromJson(Genres.insert(name: 'Test'));

Regarding the converters

.select("*") returns an array based on docs/testing so you have to apply the corresponding converter

with no .single()

  final result = await supabase.events
      .insert(Events.insert(title: "title", category: ["category"]))
      .select("*")
      .withConverter(Events.converter);

  print(result); // [Instance of 'Events']

with .single()

  final result = await supabase.events
      .insert(Events.insert(title: "title", category: ["category"]))
      .select("*")
      .single()
      .withConverter(Events.converterSingle);

  print(result); // Instance of 'Events'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants