Skip to content

Commit

Permalink
Merge pull request #109 from mmvergara/support-array-enums
Browse files Browse the repository at this point in the history
Support array enums
  • Loading branch information
mmvergara authored Dec 12, 2024
2 parents 6d9ed4d + dde492c commit 2c4b3ce
Show file tree
Hide file tree
Showing 23 changed files with 374 additions and 364 deletions.
6 changes: 4 additions & 2 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ final allBooks = await supabase

#### 1.2 Do you have serial types?

if you have serial types you need to add a `[supadart:serial]` to the column like this
We don't recommend using serial types when using this package, if you have serial types you need to add a `[supadart:serial]` to the column like this

> You probably don't have them Serial types aren't available in the Supabase editor and must be added via SQL editor manually.
```sql
COMMENT ON COLUMN test_table.bigserialx IS '[supadart:serial]';
Expand All @@ -86,7 +88,7 @@ COMMENT ON COLUMN test_table.serialx IS 'this part [supadart:serial] just needs
-- 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?](https://gist.github.com/mmvergara/5e3d42d73dd316f8ff809fb940163c1f)
> [Why do we need this?](https://gist.github.com/mmvergara/5e3d42d73dd316f8ff809fb940163c1f)
#### 1.3 Install `Internationalization` package

Expand Down
4 changes: 3 additions & 1 deletion cli/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ supadart.exe

lib/models/

config.yaml
config.yaml

supadart.yaml
3 changes: 2 additions & 1 deletion cli/.pubignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ generated_classes.dart

lib/models/
test/
DEV_DETUP.MD
DEV_DETUP.MD
supadart.yaml
60 changes: 47 additions & 13 deletions cli/bin/supadart.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:dotenv/dotenv.dart';
import 'package:supadart/config_init.dart';
import 'package:supadart/generators/index.dart';
import 'package:supadart/generators/storage/fetch_storage.dart';
Expand Down Expand Up @@ -27,9 +28,8 @@ void main(List<String> arguments) async {
}

print("🚀 Supadart $version");

final config = await loadYamlConfig(results);
final options = extractOptions(results, config);
final yamlConfig = await loadYamlConfig(results);
final options = extractOptions(results, yamlConfig);

if (!validateOptions(options)) {
print('use -h or --help for help');
Expand Down Expand Up @@ -81,28 +81,54 @@ Future<YamlMap> loadYamlConfig(ArgResults results) async {
print("Config file found");
return loadYaml(configContent);
} catch (e) {
print("Using CLI arguments only");
return YamlMap();
throw ("As of version 1.6.5 >, you need to create a config file use --init command to generate one");
}
}

Map<String, dynamic> extractOptions(ArgResults results, YamlMap config) {
// check if env values are set for SUPABASE_URL and SUPABASE_ANON_KEY
var env = DotEnv(includePlatformEnvironment: true)..load();
if (env['SUPABASE_URL'] != null && env['SUPABASE_ANON_KEY'] != null) {
print("Using .env file for SUPABASE_URL and SUPABASE_ANON_KEY");
}

// Extract enums as a Map<String, List<String>> from config['enums']
Map<String, List<String>> enums = {};
if (config.containsKey('enums')) {
if (config['enums'] != null) {
(config['enums'] as Map).forEach((enumName, value) {
if (value is List) {
enums["public.$enumName"] = List<String>.from(value);
}
});
}
}

return {
'url': results['url'] ?? config['supabase_url'] ?? '',
'anonKey': results['key'] ?? config['supabase_anon_key'] ?? '',
'url':
results['url'] ?? env['SUPABASE_URL'] ?? config['SUPABASE_URL'] ?? '',
'anonKey': results['key'] ??
env['SUPABASE_ANON_KEY'] ??
config['SUPABASE_ANON_KEY'] ??
'',
'isSeparated': results['separated'] ? true : config['separated'] ?? false,
'isDart': results['dart'] ? true : config['dart'] ?? false,
'output': results['output'] ?? config['output'] ?? './lib/models/',
'mappings': config['mappings'],
'exclude': results['exclude']?.split(',') ??
List<String>.from(config['exclude'] ?? [])
List<String>.from(config['exclude'] ?? []),
'mapOfEnums': enums,
};
}

bool validateOptions(Map<String, dynamic> options) {
if (options['url'].isEmpty || options['anonKey'].isEmpty) {
print(
"Please provide --url and --key or set supabase_url and supabase_anon_key in .yaml file");
"${red}Please Provide the url and key for your supabase instance... You can");
print("1. Use a .env file to specify SUPABASE_URL and SUPABASE_ANON_KEY");
print("2. Set SUPABASE_URL and SUPABASE_ANON_KEY in .yaml config file");
print(
"3. Specificy --url and --key in the cli (ex. supadart -u <url> -k <key>) $reset");
return false;
}
return true;
Expand All @@ -117,20 +143,27 @@ void printConfiguration(Map<String, dynamic> options) {
print('Dart: ${options['isDart']}');
print('Mappings: ${options['mappings']}');
print('Excluded: ${options['exclude']}');
print('Enums: ${options['mapOfEnums']}');
print('==============================');
}

Future<void> generateModels(Map<String, dynamic> options) async {
print("Fetching database schema...");
final databaseSwagger =
await fetchDatabaseSwagger(options['url'], options['anonKey']);
final databaseSwagger = await fetchDatabaseSwagger(
options['url'],
options['anonKey'],
options['mapOfEnums'],
);

if (databaseSwagger == null) {
print('Failed to fetch database');
exit(1);
}

final storageList =
await fetchStorageList(options['url'], options['anonKey']);
final storageList = await fetchStorageList(
options['url'],
options['anonKey'],
);
if (storageList == null) {
print('Failed to fetch storage');
exit(1);
Expand All @@ -145,6 +178,7 @@ Future<void> generateModels(Map<String, dynamic> options) async {
options['isSeparated'],
options['mappings'],
options['exclude'],
options['mapOfEnums'],
);

await generateAndFormatFiles(files, options['output']);
Expand Down
11 changes: 8 additions & 3 deletions cli/lib/config_init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ configFileInit(String path) async {

// Write to the file
file.writeAsStringSync('''
# Required (if you dont have `-u` specified)
# Don't want to expose your supabase credentials? you have two options
# 1. Use a .env file to specify SUPABASE_URL and SUPABASE_ANON_KEY
# 2. Specify --url and --key in the CLI (ex. supadart -u <url> -k <key>)
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
Expand All @@ -37,7 +40,9 @@ mappings:
# categories: category
# children: child
# people: person
# Do you have a column that is array of enum type (enum[]) type? If yes, you need to specify the enums
enums:
# mood: [happy, sad, neutral, excited, angry]
# Optional, used to exclude methods from generated classes
exclude:
# - toJson
Expand Down
14 changes: 8 additions & 6 deletions cli/lib/generators/class/from_json.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ String generateFromJsonMethod(String className, Table table) {

String decodeFromJson(Column columnDetails) {
final postgresFormat = columnDetails.postgresFormat;
final dartType = columnDetails.dartType;
String jsonValue = 'jsonn[\'${columnDetails.dbColName}\']';
String jsonDecode = "";
switch (postgresFormat) {
Expand Down Expand Up @@ -187,19 +186,22 @@ String decodeFromJson(Column columnDetails) {
jsonDecode =
"($jsonValue as List<dynamic>).map((e) => e.toString()).toList()";
break;

default:
// if no type is found it is assumed to be an enum type
String enumName = columnDetails.dartType;
return '$jsonValue != null ? $enumName.values.byName($jsonValue.toString()) : $enumName.values.first';
if (postgresFormat.contains("[]")) {
return '$jsonValue != null ? $enumName.from($jsonValue.map((e) => ${enumName.replaceAll("List<", "").replaceFirst(">", "")}.values.byName(e.toString())).toList()) : []';
} else {
return '$jsonValue != null ? $enumName.values.byName($jsonValue.toString()) : $enumName.values.first';
}
}
String code =
'$jsonValue != null ? $jsonDecode : ${dartTypeDefaultNullValue(dartType)}';
'$jsonValue != null ? $jsonDecode : ${dartTypeDefaultNullValue(columnDetails)}';
return code;
}

String dartTypeDefaultNullValue(String dartType) {
switch (dartType) {
String dartTypeDefaultNullValue(Column columnDetails) {
switch (columnDetails.dartType) {
case 'int':
return '0';
case 'BigInt':
Expand Down
5 changes: 4 additions & 1 deletion cli/lib/generators/class/generate_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,15 @@ String encodeToJson(
break;

default:
// If no format is provided, we assume it's a Enum
// print(columnName);
// print(columnDetails.enumValues);
if (columnDetails.enumValues.isNotEmpty) {
jsonEncodableType = isArray
? "$columnName.map((e) => e.toString().split('.').last).toList()"
: "$columnName.toString().split('.').last";
break;
} else {
print("No enum values found for $columnName");
}
}
return jsonEncodableType;
Expand Down
3 changes: 2 additions & 1 deletion cli/lib/generators/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ List<GeneratedFile> supadartRun(
bool isSeparated,
YamlMap? mappings,
List<String> exclude,
Map<String, List<String>> mapOfEnums,
) {
final dartClasses = generateDartClasses(swagger, mappings, exclude);

final clientExtension = generateClientExtension(swagger);
final storageClientExtension = generateStorageClientExtension(storageList);
final modelExports = generateExports(swagger, mappings);
final enums = generateEnums(swagger);
final enums = generateEnums(mapOfEnums);

bool needsIntl = false;
bool needsDartConvert = false;
Expand Down
23 changes: 4 additions & 19 deletions cli/lib/generators/standalone/enums.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,8 @@
import '../swagger/swagger.dart';

String generateEnums(DatabaseSwagger swagger) {
final enumMap = <String, String>{};
swagger.definitions.forEach((tableName, table) {
table.columns.forEach((columnName, columnDetails) {
if (columnDetails.enumValues.isNotEmpty) {
final enumName = columnDetails.postgresFormat
.split(".")
.last
.toUpperCase()
.replaceAll('"', "");
enumMap[enumName] = columnDetails.enumValues.join(", ");
}
});
});

String generateEnums(Map<String, List<String>> mapOfEnums) {
final code = StringBuffer();
enumMap.forEach((enumName, enumValues) {
code.write("enum $enumName { $enumValues }\n");
mapOfEnums.forEach((enumName, enumValues) {
code.write(
"enum ${enumName.split(".").last.toUpperCase().replaceAll('"', "")} { ${enumValues.join(", ")} }\n");
});
return code.toString();
}
26 changes: 20 additions & 6 deletions cli/lib/generators/swagger/column.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Class to represent a database column
import '../utils/string_formatters.dart';

import 'utils.dart';

class Column {
Expand All @@ -19,7 +18,7 @@ class Column {
required this.postgresFormat,
required this.dbColName,
required this.camelColName,
this.enumValues = const [],
required this.enumValues,
this.hasDefaultValue,
this.description,
this.maxLength,
Expand All @@ -30,7 +29,11 @@ class Column {

String get dartType {
if (enumValues.isNotEmpty) {
return postgresFormat.split(".").last.toUpperCase().replaceAll('"', "");
if (postgresFormat.contains("[]")) {
return "List<${postgresFormat.split(".").last.toUpperCase().replaceAll('"', "").replaceAll("[]", "")}>";
} else {
return postgresFormat.split(".").last.toUpperCase().replaceAll('"', "");
}
}
return postgresFormatToDartType(postgresFormat).type.replaceAll('"', "");
}
Expand All @@ -50,14 +53,25 @@ class Column {
String colName,
Map<String, dynamic> json,
List<String> parentTableRequiredFields,
Map<String, List<String>> mapOfEnums,
) {
List<String> enumValues = [];
if (json['format'].toString().contains("public.")) {
for (var enumName in mapOfEnums.keys) {
if (json['format'].toString().contains(enumName)) {
enumValues = mapOfEnums[enumName]!;
// print("enumValues set for ${json['format']} to $enumValues");
}
}
}

return Column(
postgresFormat: json['format'],
dbColName: colName,
camelColName: snakeCasingToCamelCasing(colName),
enumValues:
json['enum'] != null ? List<String>.from(json['enum']) : <String>[],
hasDefaultValue: json['default'] != null,
enumValues: enumValues,
hasDefaultValue: json['description']?.contains('[supadart:serial]') ??
json['default'] != null,
description: json['description'],
maxLength: json['maxLength'],
isPrimaryKey: json['description']?.contains('<pk/>') ?? false,
Expand Down
7 changes: 4 additions & 3 deletions cli/lib/generators/swagger/swagger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ class DatabaseSwagger {

DatabaseSwagger(this.definitions);

factory DatabaseSwagger.fromJson(Map<String, dynamic> json) {
factory DatabaseSwagger.fromJson(
Map<String, dynamic> json, Map<String, List<String>> mapOfEnums) {
final definitions = json['definitions'] as Map<String, dynamic>;
return DatabaseSwagger(
definitions
.map((key, value) => MapEntry(key, Table.fromJson(key, value))),
definitions.map((key, value) =>
MapEntry(key, Table.fromJson(key, value, mapOfEnums))),
);
}
}
6 changes: 4 additions & 2 deletions cli/lib/generators/swagger/table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ class Table {
required this.columns,
});

factory Table.fromJson(String name, Map<String, dynamic> json) {
factory Table.fromJson(String name, Map<String, dynamic> json,
Map<String, List<String>> mapOfEnums) {
final properties = json['properties'] as Map<String, dynamic>;
final requiredFields = json['required'] != null
? List<String>.from(json['required'])
: <String>[];

return Table(
name: name,
requiredFields: requiredFields,
columns: properties.map((key, value) => MapEntry(
snakeCasingToCamelCasing(key),
Column.fromJson(key, value, requiredFields))),
Column.fromJson(key, value, requiredFields, mapOfEnums))),
);
}
}
Loading

0 comments on commit 2c4b3ce

Please sign in to comment.