From b387cda3ddf0e8b50670597c1063e1ff8218b775 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Sat, 30 Mar 2024 14:50:21 +0100 Subject: [PATCH] feat(crud): add supabase implementation preview --- .../example/lib/main_supabase.dart | 46 ++++ packages/wyatt_crud_bloc/example/pubspec.yaml | 3 + .../wyatt_crud_bloc_supabase/.gitignore | 1 + .../wyatt_crud_bloc_supabase/.metadata | 10 + .../wyatt_crud_bloc_supabase/.pubignore | 1 + .../wyatt_crud_bloc_supabase/AUTHORS | 1 + .../wyatt_crud_bloc_supabase/CHANGELOG.md | 3 + .../wyatt_crud_bloc_supabase/LICENSE | 1 + .../wyatt_crud_bloc_supabase/README.md | 39 +++ .../analysis_options.yaml | 1 + .../data/crud_data_source_supabase_impl.dart | 255 ++++++++++++++++++ .../lib/wyatt_crud_bloc_supabase.dart | 20 ++ .../wyatt_crud_bloc_supabase/pubspec.yaml | 35 +++ 13 files changed, 416 insertions(+) create mode 100644 packages/wyatt_crud_bloc/example/lib/main_supabase.dart create mode 120000 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.gitignore create mode 100644 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.metadata create mode 120000 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.pubignore create mode 120000 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/AUTHORS create mode 100644 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/CHANGELOG.md create mode 120000 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/LICENSE create mode 100644 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/README.md create mode 100644 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/analysis_options.yaml create mode 100644 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/lib/src/data/crud_data_source_supabase_impl.dart create mode 100644 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/lib/wyatt_crud_bloc_supabase.dart create mode 100644 packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/pubspec.yaml diff --git a/packages/wyatt_crud_bloc/example/lib/main_supabase.dart b/packages/wyatt_crud_bloc/example/lib/main_supabase.dart new file mode 100644 index 00000000..79b77a05 --- /dev/null +++ b/packages/wyatt_crud_bloc/example/lib/main_supabase.dart @@ -0,0 +1,46 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:crud_bloc_example/app.dart'; +import 'package:crud_bloc_example/app_bloc_observer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:wyatt_crud_bloc/wyatt_crud_bloc.dart'; +import 'package:wyatt_crud_bloc_supabase/wyatt_crud_bloc_supabase.dart'; + +const supabaseUrl = 'https://yarkjotsulnccxdkztiv.supabase.co'; +const supabaseKey = String.fromEnvironment('SUPABASE_KEY'); + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + Bloc.observer = AppBlocObserver(); + + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + ); + + final CrudDataSource crudDataSource = CrudDataSourceSupabaseImpl( + 'users', + id: (_, json) => json['id'] as String, + getIdKey: (_) => 'id', + ); + + runApp(MyApp( + crudDataSource: crudDataSource, + )); +} diff --git a/packages/wyatt_crud_bloc/example/pubspec.yaml b/packages/wyatt_crud_bloc/example/pubspec.yaml index 992e0738..d9dc4c89 100644 --- a/packages/wyatt_crud_bloc/example/pubspec.yaml +++ b/packages/wyatt_crud_bloc/example/pubspec.yaml @@ -33,11 +33,14 @@ dependencies: equatable: ^2.0.5 firebase_core: ^2.22.0 cloud_firestore: ^4.13.0 + supabase_flutter: ^2.3.4 flutter_bloc: ^8.1.3 wyatt_crud_bloc: path: "../" wyatt_crud_bloc_firestore: path: "../wyatt_crud_bloc_firestore" + wyatt_crud_bloc_supabase: + path: "../wyatt_crud_bloc_supabase" wyatt_architecture: hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.gitignore b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.gitignore new file mode 120000 index 00000000..00e03899 --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.gitignore @@ -0,0 +1 @@ +../../../.gitignore \ No newline at end of file diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.metadata b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.metadata new file mode 100644 index 00000000..6176c000 --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d211f42860350d914a5ad8102f9ec32764dc6d06" + channel: "stable" + +project_type: package diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.pubignore b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.pubignore new file mode 120000 index 00000000..353b763d --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/.pubignore @@ -0,0 +1 @@ +../../../.pubignore \ No newline at end of file diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/AUTHORS b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/AUTHORS new file mode 120000 index 00000000..d776b73d --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/AUTHORS @@ -0,0 +1 @@ +../../../AUTHORS \ No newline at end of file diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/CHANGELOG.md b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/LICENSE b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/LICENSE new file mode 120000 index 00000000..5853aaea --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/README.md b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/README.md new file mode 100644 index 00000000..02fe8eca --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/analysis_options.yaml b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/analysis_options.yaml new file mode 100644 index 00000000..8c9daa4e --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/analysis_options.yaml @@ -0,0 +1 @@ +include: package:wyatt_analysis/analysis_options.flutter.yaml diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/lib/src/data/crud_data_source_supabase_impl.dart b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/lib/src/data/crud_data_source_supabase_impl.dart new file mode 100644 index 00000000..46ee0e29 --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/lib/src/data/crud_data_source_supabase_impl.dart @@ -0,0 +1,255 @@ +// Copyright (C) 2023 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:wyatt_crud_bloc/wyatt_crud_bloc.dart' as crud show Query; +import 'package:wyatt_crud_bloc/wyatt_crud_bloc.dart' hide Query; + +typedef QueryBuilder = PostgrestTransformBuilder>>; +typedef StreamBuilder = SupabaseStreamBuilder; + +/// {@template crud_data_source_supabase_impl} +/// A concrete implementation of [CrudDataSource] that uses +/// [Supabase] as the data source. +/// {@endtemplate} +class CrudDataSourceSupabaseImpl extends CrudDataSource { + /// {@macro crud_data_source_supabase_impl} + CrudDataSourceSupabaseImpl( + String table, { + required super.id, + this.getIdKey, + SupabaseClient? supabase, + }) : _supabase = supabase ?? Supabase.instance.client { + _supabaseQueryBuilder = _supabase.from(table); + } + + final SupabaseClient _supabase; + late SupabaseQueryBuilder _supabaseQueryBuilder; + + /// Get the id field name depending on the operation type. + /// If null, 'id' will be used. + final String? Function( + OperationType operationType, + )? getIdKey; + + @override + Future create(Map object, {String? id}) { + if (id != null) { + return _supabaseQueryBuilder.upsert(object); + } else { + final String? id = this.id( + OperationType.create, + object, + ); + + if (id != null) { + return _supabaseQueryBuilder.upsert(object); + } + return _supabaseQueryBuilder.insert(object); + } + } + + @override + Future delete(String id) { + final String idKey = getIdKey?.call(OperationType.delete) ?? 'id'; + return _supabaseQueryBuilder.delete().match({idKey: id}); + } + + @override + Future deleteAll() async { + final List> snapshots = + await _supabaseQueryBuilder.select(); + for (final Map snapshot in snapshots) { + final String idKey = getIdKey?.call(OperationType.delete) ?? 'id'; + await _supabaseQueryBuilder.delete().match({idKey: snapshot[idKey]}); + } + } + + @override + Future?> get(String id) async { + final String idKey = getIdKey?.call(OperationType.read) ?? 'id'; + final List> snapshots = + await _supabaseQueryBuilder.select().match({idKey: id}); + return snapshots.firstOrNull; + } + + @override + Future?>> getAll() async => + _supabaseQueryBuilder.select(); + + @override + Future?>> search( + List conditions, + ) async { + final PostgrestFilterBuilder>> query = + _supabaseQueryBuilder.select(); + + final filter = conditions.whereType>().firstOrNull; + final modifiers = [ + ...conditions.whereType(), + ...conditions.whereType(), + ]; + + PostgrestTransformBuilder>>? queryTransform; + + if (filter != null) { + queryTransform = _queryFilterParser(filter, query); + } + + for (final condition in modifiers) { + queryTransform = _queryModifierParser(condition, queryTransform ?? query); + } + + return query; + } + + @override + Stream?>> stream({ + String? id, + List? conditions, + bool includeMetadataChanges = false, + }) { + // TODO(hpcl): use conditions!! + final stream = _supabaseQueryBuilder + .stream(primaryKey: [getIdKey?.call(OperationType.read) ?? 'id']); + + if (id != null) { + return stream.eq( + getIdKey?.call(OperationType.read) ?? 'id', + id, + ); + } else { + return stream; + } + } + + @override + Future update( + String id, { + Map? object, + }) async { + if (object != null) { + final String idKey = getIdKey?.call(OperationType.update) ?? 'id'; + return _supabaseQueryBuilder.update(object).match({idKey: id}); + } else { + throw Exception('You must provide data to update'); + } + } + + @override + Future updateAll(Map? data) async { + if (data == null) { + throw Exception('You must provide data to update'); + } + return _supabaseQueryBuilder.update(data); + } + + QueryBuilder _queryFilterParser( + crud.Query condition, + PostgrestFilterBuilder>> query, + ) { + if (condition is WhereQuery) { + switch (condition.type) { + case WhereQueryType.isEqualTo: + return query.eq(condition.field, condition.value as Object); + case WhereQueryType.isNotEqualTo: + return query.neq(condition.field, condition.value as Object); + case WhereQueryType.isLessThan: + return query.lt(condition.field, condition.value as Object); + case WhereQueryType.isLessThanOrEqualTo: + return query.lte(condition.field, condition.value as Object); + case WhereQueryType.isGreaterThan: + return query.gt(condition.field, condition.value as Object); + case WhereQueryType.isGreaterThanOrEqualTo: + return query.gte(condition.field, condition.value as Object); + case WhereQueryType.arrayContains: + return query.contains(condition.field, condition.value as Object); + case WhereQueryType.whereIn: + return query.inFilter( + condition.field, + condition.value as List, + ); + case WhereQueryType.whereNotIn: + return query.not( + condition.field, + 'in', + condition.value as List, + ); + case WhereQueryType.isNull: + if (condition.value as bool) { + return query.isFilter(condition.field, null); + } else { + return query.not(condition.field, 'is', null); + } + case WhereQueryType.arrayContainsAny: + throw UnsupportedError( + '${condition.type} is not supported by Supabase', + ); + } + } + return query; + } + + StreamBuilder _streamFilterParser( + crud.Query condition, + SupabaseStreamFilterBuilder query, + ) { + if (condition is WhereQuery) { + switch (condition.type) { + case WhereQueryType.isEqualTo: + return query.eq(condition.field, condition.value as Object); + case WhereQueryType.isNotEqualTo: + return query.neq(condition.field, condition.value as Object); + case WhereQueryType.isLessThan: + return query.lt(condition.field, condition.value as Object); + case WhereQueryType.isLessThanOrEqualTo: + return query.lte(condition.field, condition.value as Object); + case WhereQueryType.isGreaterThan: + return query.gt(condition.field, condition.value as Object); + case WhereQueryType.isGreaterThanOrEqualTo: + return query.gte(condition.field, condition.value as Object); + case WhereQueryType.whereIn: + return query.inFilter( + condition.field, + condition.value as List, + ); + case WhereQueryType.arrayContains: + case WhereQueryType.arrayContainsAny: + case WhereQueryType.whereNotIn: + case WhereQueryType.isNull: + throw UnsupportedError( + '${condition.type} is not supported in stream by Supabase', + ); + } + } + return query; + } + + QueryBuilder _queryModifierParser( + crud.Query condition, + QueryBuilder query, + ) { + if (condition is LimitQuery) { + return query.limit(condition.limit); + } else if (condition is OrderByQuery) { + return query.order( + condition.field, + ascending: condition.ascending, + ); + } + return query; + } +} diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/lib/wyatt_crud_bloc_supabase.dart b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/lib/wyatt_crud_bloc_supabase.dart new file mode 100644 index 00000000..1afb38a8 --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/lib/wyatt_crud_bloc_supabase.dart @@ -0,0 +1,20 @@ +// Copyright (C) 2023 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/// Create/Read/Update/Delete BLoC implementation for Supabase. +library wyatt_crud_bloc_supabase; + +export 'src/data/crud_data_source_supabase_impl.dart'; diff --git a/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/pubspec.yaml b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/pubspec.yaml new file mode 100644 index 00000000..f1eaa56d --- /dev/null +++ b/packages/wyatt_crud_bloc/wyatt_crud_bloc_supabase/pubspec.yaml @@ -0,0 +1,35 @@ +name: wyatt_crud_bloc_supabase +description: Create/Read/Update/Delete implementation for Supabase +repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_crud_bloc +version: 0.2.0 + +publish_to: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + flutter: { sdk: flutter } + + flutter_bloc: ^8.1.1 + equatable: ^2.0.5 + supabase_flutter: ^2.3.4 + + wyatt_architecture: + hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ + version: ^0.2.0+1 + + wyatt_type_utils: + hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ + version: ^0.0.5 + + wyatt_crud_bloc: + hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ + version: ^0.2.0 + +dev_dependencies: + flutter_test: { sdk: flutter } + bloc_test: ^9.1.0 + wyatt_analysis: + hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub + version: ^2.6.1 -- 2.47.2