From a4d361409b2a86b191abbb8606667cc22a03a9c4 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Fri, 9 Oct 2026 15:43:56 +0200 Subject: [PATCH] refactor!: remove SupabaseClientPlugin and the public PostgrestTableExecutor The plugin seam and the executor extension point from #1857 were merged before their design was settled. Remove them from the public API; the typed builders keep collecting a PostgrestTableRequest and executing it over HTTP internally. --- AGENTS.md | 6 +- MIGRATION.md | 4 - packages/postgrest/lib/postgrest.dart | 13 +- packages/postgrest/lib/src/postgrest.dart | 15 +- .../lib/src/postgrest_typed_builder.dart | 24 +- .../src/postgrest_typed_filter_builder.dart | 18 +- .../src/postgrest_typed_query_builder.dart | 6 +- .../postgrest_typed_transform_builder.dart | 44 +- .../postgrest/test/table_executor_test.dart | 498 ------------------ packages/postgrest/test/typed_query_test.dart | 11 +- .../supabase/lib/src/supabase_client.dart | 61 --- .../lib/src/supabase_client_plugin.dart | 50 -- .../lib/src/supabase_query_schema.dart | 13 +- .../lib/src/supabase_typed_query_builder.dart | 6 +- packages/supabase/lib/supabase.dart | 1 - .../supabase/test/client_plugin_test.dart | 199 ------- .../supabase_flutter/lib/src/supabase.dart | 32 -- .../supabase_flutter/test/plugin_test.dart | 143 ----- .../lib/src/test_supabase_client.dart | 3 - sdk-compliance.yaml | 67 --- 20 files changed, 77 insertions(+), 1137 deletions(-) delete mode 100644 packages/postgrest/test/table_executor_test.dart delete mode 100644 packages/supabase/lib/src/supabase_client_plugin.dart delete mode 100644 packages/supabase/test/client_plugin_test.dart delete mode 100644 packages/supabase_flutter/test/plugin_test.dart diff --git a/AGENTS.md b/AGENTS.md index d3cbb5414..4b12edebf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -168,9 +168,9 @@ not wrap untyped builders. Each method returns a copy holding an immutable `Post that describes the query as structured values (operation, columns, filter, orderings, payload, and `PostgrestRequestOptions` for headers, retry, timeout and abort signal). Awaiting the builder hands the request to a `PostgrestTableExecutor` and converts the `PostgrestTableResult` into the table's -`Row` type. The default `PostgrestHttpTableExecutor` renders the request through the untyped -builders at execution time, so both APIs send the same wire request; another executor (a -`SupabaseClientPlugin` can wrap it) may cache, queue or answer from a local store instead. +`Row` type. `PostgrestHttpTableExecutor` renders the request through the untyped builders at +execution time, so both APIs send the same wire request. These types are internal to the postgrest +package and not exported. ### Realtime Architecture diff --git a/MIGRATION.md b/MIGRATION.md index 382451bd2..efbe4d2db 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2240,10 +2240,6 @@ are checked against the column types at compile time, and writes only accept the and update types. `from()` keeps working, and both can be used on one client, so a codebase can move over one table at a time. -Requests made through `table()` run through the executor that `SupabaseClientPlugin`s wrap, so -features built as plugins, such as offline support, only apply to them. Requests made through -`from()` bypass every plugin. - The typed API is marked `@experimental` while it settles. Referencing an experimental type such as `PostgrestFilter` directly reports `experimental_member_use`, which an `// ignore_for_file: experimental_member_use` silences; the generated file already carries it. diff --git a/packages/postgrest/lib/postgrest.dart b/packages/postgrest/lib/postgrest.dart index 211ed0a11..78e6a9529 100644 --- a/packages/postgrest/lib/postgrest.dart +++ b/packages/postgrest/lib/postgrest.dart @@ -14,6 +14,17 @@ export 'package:yet_another_json_isolate/yet_another_json_isolate.dart' export 'src/postgrest.dart'; export 'src/postgrest_builder.dart'; -export 'src/postgrest_typed_builder.dart' hide renderPostgrestQueryValue; +export 'src/postgrest_typed_builder.dart' + hide + PostgrestEmbeddedPage, + PostgrestExplainOptions, + PostgrestHttpTableExecutor, + PostgrestRequestOptions, + PostgrestResultShape, + PostgrestTableExecutor, + PostgrestTableOperation, + PostgrestTableRequest, + PostgrestTableResult, + renderPostgrestQueryValue; export 'src/types.dart'; export 'package:http/http.dart' show RequestAbortedException; diff --git a/packages/postgrest/lib/src/postgrest.dart b/packages/postgrest/lib/src/postgrest.dart index b80217fb9..ebefb47f6 100644 --- a/packages/postgrest/lib/src/postgrest.dart +++ b/packages/postgrest/lib/src/postgrest.dart @@ -165,21 +165,12 @@ class PostgrestClient { /// .select() /// .where(Books.id.gt(10)); /// ``` - /// - /// The builder collects a [PostgrestTableRequest] and hands it to - /// [executor] when awaited; without one the request is sent to PostgREST - /// through this client by a [PostgrestHttpTableExecutor]. @experimental PostgrestTypedQueryBuilder table( - PostgrestTable table, { - PostgrestTableExecutor? executor, - }) { + PostgrestTable table, + ) { final schema = _schemaIsExplicit ? _schema : (table.schema ?? _schema); - return PostgrestTypedQueryBuilder( - table, - executor: executor ?? PostgrestHttpTableExecutor(this), - schema: schema, - ); + return PostgrestTypedQueryBuilder(table, client: this, schema: schema); } /// Select a schema to query or perform an function (rpc) call. diff --git a/packages/postgrest/lib/src/postgrest_typed_builder.dart b/packages/postgrest/lib/src/postgrest_typed_builder.dart index b6b3be298..037e48236 100644 --- a/packages/postgrest/lib/src/postgrest_typed_builder.dart +++ b/packages/postgrest/lib/src/postgrest_typed_builder.dart @@ -229,24 +229,22 @@ extension PostgrestTypedRowsBuilder /// A typed PostgREST request that can be awaited. /// -/// Holds the [request] the builder methods have collected so far and the -/// [PostgrestTableExecutor] that runs it. Awaiting the builder executes the -/// request and converts the result into [T], so awaiting it never exposes raw -/// `Map` data. +/// Holds the request the builder methods have collected so far. Awaiting the +/// builder executes the request and converts the result into [T], so +/// awaiting it never exposes raw `Map` data. @experimental class PostgrestTypedBuilder implements Future { - const PostgrestTypedBuilder._(this.request, this._executor, this._convert); + const PostgrestTypedBuilder._(this._request, this._executor, this._convert); - /// The request awaiting this builder executes. - final PostgrestTableRequest request; + final PostgrestTableRequest _request; final PostgrestTableExecutor _executor; final _ResultConverter _convert; - /// Runs [request] on the executor, routing a synchronous throw into the + /// Runs [_request] on the executor, routing a synchronous throw into the /// returned future so the [Future] contract holds for every executor. Future _execute() => - Future.sync(() => _executor.execute(request)).then(_convert); + Future.sync(() => _executor.execute(_request)).then(_convert); PostgrestTypedBuilder _with(PostgrestTableRequest changed) => PostgrestTypedBuilder._(changed, _executor, _convert); @@ -255,7 +253,7 @@ class PostgrestTypedBuilder implements Future { /// /// See [PostgrestBuilder.retry] for [enabled] and [count]. PostgrestTypedBuilder retry({bool enabled = true, int? count}) => - _with(request._retry(enabled, count)); + _with(_request._retry(enabled, count)); /// Bounds how long a single attempt of this request may take, overriding /// the timeout configured on [PostgrestClient]. @@ -263,7 +261,7 @@ class PostgrestTypedBuilder implements Future { /// Unlike [timeout], which only stops waiting for the result, this cancels /// the attempt. See [PostgrestBuilder.requestTimeout]. PostgrestTypedBuilder requestTimeout(Duration timeout) => - _with(request._requestTimeout(timeout)); + _with(_request._requestTimeout(timeout)); /// Cancels the request when [abortSignal] completes, throwing a /// [RequestAbortedException] and stopping any retries. @@ -278,11 +276,11 @@ class PostgrestTypedBuilder implements Future { /// /// See [PostgrestBuilder.abortSignal]. PostgrestTypedBuilder abortSignal(Future abortSignal) => - _with(request._abortSignal(abortSignal)); + _with(_request._abortSignal(abortSignal)); /// Sets [key] to [value] in the headers of this request. PostgrestTypedBuilder setHeader(String key, String value) => - _with(request._header(key, value)); + _with(_request._header(key, value)); /// A broadcast stream of the one result. The request runs when the first /// listener subscribes, so a listener added later still receives it. diff --git a/packages/postgrest/lib/src/postgrest_typed_filter_builder.dart b/packages/postgrest/lib/src/postgrest_typed_filter_builder.dart index 00ea71de6..43a8baab3 100644 --- a/packages/postgrest/lib/src/postgrest_typed_filter_builder.dart +++ b/packages/postgrest/lib/src/postgrest_typed_filter_builder.dart @@ -8,16 +8,16 @@ part of 'postgrest_typed_builder.dart'; class PostgrestTypedFilterBuilder extends PostgrestTypedTransformBuilder { const PostgrestTypedFilterBuilder._( - super.request, + super._request, super.executor, super.convert, super.rowFromJson, ) : super._(); PostgrestTypedFilterBuilder _filtered( - PostgrestTableRequest request, + PostgrestTableRequest changed, ) => PostgrestTypedFilterBuilder._( - request, + changed, _executor, _convert, _rowFromJson, @@ -27,19 +27,19 @@ class PostgrestTypedFilterBuilder PostgrestTypedFilterBuilder retry({ bool enabled = true, int? count, - }) => _filtered(request._retry(enabled, count)); + }) => _filtered(_request._retry(enabled, count)); @override PostgrestTypedFilterBuilder requestTimeout(Duration timeout) => - _filtered(request._requestTimeout(timeout)); + _filtered(_request._requestTimeout(timeout)); @override PostgrestTypedFilterBuilder abortSignal(Future abortSignal) => - _filtered(request._abortSignal(abortSignal)); + _filtered(_request._abortSignal(abortSignal)); @override PostgrestTypedFilterBuilder setHeader(String key, String value) => - _filtered(request._header(key, value)); + _filtered(_request._header(key, value)); /// Only rows satisfying [filter]. /// @@ -65,9 +65,9 @@ class PostgrestTypedFilterBuilder /// /// Repeated [where] calls combine with logical AND. PostgrestTypedFilterBuilder where(PostgrestFilter filter) { - final current = request.filter; + final current = _request.filter; return _filtered( - request.copyWith(filter: current == null ? filter : current & filter), + _request.copyWith(filter: current == null ? filter : current & filter), ); } } diff --git a/packages/postgrest/lib/src/postgrest_typed_query_builder.dart b/packages/postgrest/lib/src/postgrest_typed_query_builder.dart index e3516ed1e..2a80408fc 100644 --- a/packages/postgrest/lib/src/postgrest_typed_query_builder.dart +++ b/packages/postgrest/lib/src/postgrest_typed_query_builder.dart @@ -11,11 +11,11 @@ part of 'postgrest_typed_builder.dart'; @experimental class PostgrestTypedQueryBuilder { /// {@macro postgrest_typed_query_builder} - const PostgrestTypedQueryBuilder( + PostgrestTypedQueryBuilder( this.table, { - required PostgrestTableExecutor executor, + required PostgrestClient client, String? schema, - }) : _executor = executor, + }) : _executor = PostgrestHttpTableExecutor(client), _schema = schema, _options = null; diff --git a/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart b/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart index 1977b2403..b8affe3ca 100644 --- a/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart +++ b/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart @@ -7,7 +7,7 @@ part of 'postgrest_typed_builder.dart'; @experimental class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { const PostgrestTypedTransformBuilder._( - super.request, + super._request, super.executor, super.convert, this._rowFromJson, @@ -28,7 +28,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { PostgrestResultShape shape, _ResultConverter convert, ) => PostgrestTypedTransformBuilder._( - request.copyWith(shape: shape), + _request.copyWith(shape: shape), _executor, convert, _rowFromJson, @@ -38,20 +38,20 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { PostgrestTypedTransformBuilder retry({ bool enabled = true, int? count, - }) => _with(request._retry(enabled, count)); + }) => _with(_request._retry(enabled, count)); @override PostgrestTypedTransformBuilder requestTimeout(Duration timeout) => - _with(request._requestTimeout(timeout)); + _with(_request._requestTimeout(timeout)); @override PostgrestTypedTransformBuilder abortSignal( Future abortSignal, - ) => _with(request._abortSignal(abortSignal)); + ) => _with(_request._abortSignal(abortSignal)); @override PostgrestTypedTransformBuilder setHeader(String key, String value) => - _with(request._header(key, value)); + _with(_request._header(key, value)); /// Performs horizontal filtering with SELECT, returning the affected rows /// typed as [Row]. @@ -67,7 +67,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { /// See [selectOnly] to return some columns only. PostgrestTypedTransformBuilder> select() => PostgrestTypedTransformBuilder._( - request.copyWith( + _request.copyWith( columns: const [], shape: PostgrestResultShape.rows, returning: true, @@ -92,7 +92,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { PostgrestTypedTransformBuilder>> selectOnly(List> columns) => PostgrestTypedTransformBuilder._( - request.copyWith( + _request.copyWith( columns: _checkedSelections(columns, 'columns'), shape: PostgrestResultShape.rows, returning: true, @@ -116,7 +116,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { /// Repeated calls append, so the second key breaks ties in the first. PostgrestTypedTransformBuilder order( PostgrestOrdering ordering, - ) => _with(request.copyWith(orderings: [...request.orderings, ordering])); + ) => _with(_request.copyWith(orderings: [..._request.orderings, ordering])); /// Limits the result with the specified [count]. PostgrestTypedTransformBuilder limit( @@ -124,10 +124,10 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { String? referencedTable, }) => _with( referencedTable == null - ? request.copyWith(limit: count) - : request.copyWith( + ? _request.copyWith(limit: count) + : _request.copyWith( embeddedPages: [ - ...request.embeddedPages, + ..._request.embeddedPages, PostgrestEmbeddedPage(referencedTable, limit: count), ], ), @@ -140,10 +140,10 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { String? referencedTable, }) => _with( referencedTable == null - ? request.copyWith(offset: from, limit: to - from + 1) - : request.copyWith( + ? _request.copyWith(offset: from, limit: to - from + 1) + : _request.copyWith( embeddedPages: [ - ...request.embeddedPages, + ..._request.embeddedPages, PostgrestEmbeddedPage( referencedTable, limit: to - from + 1, @@ -159,7 +159,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { /// requires PostgREST 11.2 or higher. It applies to row and [single] /// responses, in either call order. PostgrestTypedTransformBuilder stripNulls() => - _with(request.copyWith(stripNulls: true)); + _with(_request.copyWith(stripNulls: true)); /// Runs the query but rolls back the transaction, so no changes are /// persisted. @@ -171,7 +171,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { /// await client.table(Books.table).insert(BookInsert(title: 'foo')).dryRun(); /// ``` PostgrestTypedTransformBuilder dryRun() => - _with(request.copyWith(dryRun: true)); + _with(_request.copyWith(dryRun: true)); /// Sets the maximum number of rows that can be affected by the query. /// @@ -186,7 +186,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { /// .maxAffected(10); /// ``` PostgrestTypedTransformBuilder maxAffected(int value) => - _with(request.copyWith(maxAffected: value)); + _with(_request.copyWith(maxAffected: value)); /// Retrieves the response as CSV. /// @@ -206,7 +206,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { /// await client.table(Books.table).select().head(); /// ``` PostgrestTypedBuilder head() => PostgrestTypedBuilder._( - request.copyWith(shape: PostgrestResultShape.head), + _request.copyWith(shape: PostgrestResultShape.head), _executor, _noResult, ); @@ -219,7 +219,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { /// https://supabase.com/docs/guides/database/extensions/postgis PostgrestTypedBuilder> geojson() => PostgrestTypedBuilder._( - request.copyWith(shape: PostgrestResultShape.geojson), + _request.copyWith(shape: PostgrestResultShape.geojson), _executor, (result) => result.data! as Map, ); @@ -241,7 +241,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { bool wal = false, ExplainFormat format = ExplainFormat.text, }) => PostgrestTypedBuilder._( - request.copyWith( + _request.copyWith( shape: PostgrestResultShape.explain, explainOptions: PostgrestExplainOptions( analyze: analyze, @@ -270,7 +270,7 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { PostgrestTypedBuilder> count([ CountOption option = CountOption.exact, ]) => PostgrestTypedBuilder._( - request.copyWith(countOption: option), + _request.copyWith(countOption: option), _executor, (result) => PostgrestResponse( data: _convert(result), diff --git a/packages/postgrest/test/table_executor_test.dart b/packages/postgrest/test/table_executor_test.dart deleted file mode 100644 index 07bc235b9..000000000 --- a/packages/postgrest/test/table_executor_test.dart +++ /dev/null @@ -1,498 +0,0 @@ -// The typed table access API under test is annotated @experimental. -// ignore_for_file: experimental_member_use - -import 'dart:async'; - -import 'package:postgrest/postgrest.dart'; -import 'package:supabase_test/supabase_test.dart'; -import 'package:test/test.dart'; - -extension type const Book(Map _json) - implements Map { - int get id => _json['id'] as int; - String get title => _json['title'] as String; -} - -extension type const BookInsert._(Map _json) - implements Object { - BookInsert({required String title}) : this._({'title': title}); -} - -extension type const BookUpdate._(Map _json) - implements Object { - BookUpdate({String? title}) : this._({'title': ?title}); -} - -class Books { - static const table = PostgrestTable( - 'books', - Book.new, - primaryKey: [id], - ); - static const id = PostgrestColumn('id'); - static const title = PostgrestColumn('title'); -} - -const bookRows = [ - {'id': 1, 'title': 'a'}, - {'id': 2, 'title': 'b'}, -]; - -/// Answers every request with [result] and keeps what it was asked. -class RecordingExecutor implements PostgrestTableExecutor { - RecordingExecutor([this.result = const PostgrestTableResult()]); - - final PostgrestTableResult result; - final requests = []; - - PostgrestTableRequest get onlyRequest => requests.single; - - @override - Future execute(PostgrestTableRequest request) async { - requests.add(request); - return result; - } -} - -class _FailingExecutor implements PostgrestTableExecutor { - const _FailingExecutor(); - - @override - Future execute(PostgrestTableRequest request) async => - throw StateError('offline'); -} - -void main() { - late MockSupabaseHttpClient httpClient; - late PostgrestClient client; - - setUp(() { - httpClient = MockSupabaseHttpClient()..stub(bookRows); - client = PostgrestClient( - 'http://localhost/rest/v1', - httpClient: httpClient, - ); - }); - - tearDown(() async { - await client.dispose(); - }); - - group('request value', () { - test( - 'select collects the table, columns, filters, order and page', - () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: bookRows), - ); - - final books = await client - .table(Books.table, executor: executor) - .selectOnly([Books.id, Books.title]) - .where(Books.id.gt(1)) - .where(Books.title.eq('a')) - .order(Books.title.desc()) - .range(10, 19); - - final request = executor.onlyRequest; - expect(request.table.name, 'books'); - expect(request.schema, isNull); - expect(request.operation, PostgrestTableOperation.select); - expect(request.shape, PostgrestResultShape.rows); - expect(request.columns.map((column) => column.expression), [ - 'id', - 'title', - ]); - // ignore: invalid_use_of_internal_member - expect(request.filter!.queryParameters, [ - (key: 'id', value: 'gt.1'), - (key: 'title', value: 'eq.a'), - ]); - expect(request.orderings.single.orderKey, 'title.desc'); - expect(request.offset, 10); - expect(request.limit, 10); - expect(books.map((book) => book.read(Books.title)), ['a', 'b']); - }, - ); - - test('carries the request options from every phase', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: bookRows), - ); - final abortSignal = Completer().future; - - await client - .table(Books.table, executor: executor) - .setHeader('X-Query', 'query') - .retry(enabled: false) - .select() - .requestTimeout(const Duration(seconds: 3)) - .order(Books.title) - .abortSignal(abortSignal) - .setHeader('X-Transform', 'transform'); - - final options = executor.onlyRequest.options; - expect(options.headers, { - 'X-Query': 'query', - 'X-Transform': 'transform', - }); - expect(options.retryEnabled, isFalse); - expect(options.retryCount, isNull); - expect(options.requestTimeout, const Duration(seconds: 3)); - expect(options.abortSignal, same(abortSignal)); - }); - - test('has no request options unless one is set', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: bookRows), - ); - - await client.table(Books.table, executor: executor).select(); - - final options = executor.onlyRequest.options; - expect(options.headers, isEmpty); - expect(options.retryEnabled, isNull); - expect(options.retryCount, isNull); - expect(options.requestTimeout, isNull); - expect(options.abortSignal, isNull); - }); - - test('carries the schema of the client', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: >[]), - ); - - await client - .schema('library') - .table(Books.table, executor: executor) - .select(); - - expect(executor.onlyRequest.schema, 'library'); - }); - - test('an embedded limit is kept apart from the table page', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: >[]), - ); - - await client - .table(Books.table, executor: executor) - .select() - .limit(5) - .limit(2, referencedTable: 'authors'); - - final request = executor.onlyRequest; - expect(request.limit, 5); - expect(request.embeddedPages.single.referencedTable, 'authors'); - expect(request.embeddedPages.single.limit, 2); - }); - - test('single converts the one returned row', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: {'id': 1, 'title': 'a'}), - ); - - final Book book = await client - .table(Books.table, executor: executor) - .select() - .single(); - - expect(executor.onlyRequest.shape, PostgrestResultShape.single); - expect(book.title, 'a'); - }); - - test('maybeSingle converts a missing row into null', () async { - final executor = RecordingExecutor(); - - final Book? book = await client - .table(Books.table, executor: executor) - .select() - .maybeSingle(); - - expect(executor.onlyRequest.shape, PostgrestResultShape.maybeSingle); - expect(book == null, isTrue); - }); - - test('a mutation carries its payload and asks nothing back', () async { - final executor = RecordingExecutor(); - - await client - .table(Books.table, executor: executor) - .update(BookUpdate(title: 'b')) - .where(Books.id.eq(1)); - - final request = executor.onlyRequest; - expect(request.operation, PostgrestTableOperation.update); - expect(request.payload, {'title': 'b'}); - expect(request.shape, PostgrestResultShape.none); - expect(request.returning, isFalse); - expect(request.filter!.comparison!.operator, PostgrestFilterOperator.eq); - }); - - test('a trailing select on a mutation asks the rows back', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: bookRows), - ); - - final books = await client - .table(Books.table, executor: executor) - .insertAll([BookInsert(title: 'a'), BookInsert(title: 'b')]) - .selectOnly([Books.id]); - - final request = executor.onlyRequest; - expect(request.operation, PostgrestTableOperation.insert); - expect(request.payload, [ - {'title': 'a'}, - {'title': 'b'}, - ]); - expect(request.shape, PostgrestResultShape.rows); - expect(request.returning, isTrue); - expect(request.columns.single.expression, 'id'); - expect(books.length, 2); - }); - - test('upsert carries the conflict target and flags', () async { - final executor = RecordingExecutor(); - - await client - .table(Books.table, executor: executor) - .upsert( - BookInsert(title: 'a'), - onConflict: [Books.title], - ignoreDuplicates: true, - defaultToNull: false, - ); - - final request = executor.onlyRequest; - expect(request.operation, PostgrestTableOperation.upsert); - expect(request.onConflict!.single.name, 'title'); - expect(request.ignoreDuplicates, isTrue); - expect(request.defaultToNull, isFalse); - }); - - test('count reads the count of the result', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(count: 7), - ); - - final int count = await client - .table(Books.table, executor: executor) - .count(CountOption.estimated); - - expect(executor.onlyRequest.operation, PostgrestTableOperation.count); - expect(executor.onlyRequest.countOption, CountOption.estimated); - expect(count, 7); - }); - - test('a counted select wraps rows and count', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: bookRows, count: 12), - ); - - final response = await client - .table(Books.table, executor: executor) - .select() - .count(); - - expect(executor.onlyRequest.countOption, CountOption.exact); - expect(response.count, 12); - expect(response.data.map((book) => book.id), [1, 2]); - }); - - test('csv, head and explain set their shapes', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: 'id,title'), - ); - final builder = client.table(Books.table, executor: executor).select(); - - await builder.csv(); - await builder.head(); - await builder.explain(analyze: true); - - expect(executor.requests.map((request) => request.shape), [ - PostgrestResultShape.csv, - PostgrestResultShape.head, - PostgrestResultShape.explain, - ]); - expect(executor.requests.last.explainOptions!.analyze, isTrue); - }); - - test('an empty column list is rejected when select is called', () { - expect( - () => client - .table(Books.table, executor: RecordingExecutor()) - .selectOnly([]), - throwsArgumentError, - ); - }); - - test('the request copies the collections it is given', () { - final columns = >[Books.id]; - final request = PostgrestTableRequest( - table: Books.table, - operation: PostgrestTableOperation.select, - columns: columns, - ); - - columns.clear(); - - expect(request.columns, hasLength(1)); - expect(() => request.columns.clear(), throwsUnsupportedError); - expect(() => request.orderings.clear(), throwsUnsupportedError); - }); - - test('asStream delivers the result to a listener added later', () async { - final stream = client - .table( - Books.table, - executor: RecordingExecutor( - const PostgrestTableResult(data: bookRows), - ), - ) - .select() - .asStream(); - - await Future.delayed(Duration.zero); - - expect(await stream.first, hasLength(2)); - }); - - test('asStream forwards an executor failure', () async { - final stream = client - .table(Books.table, executor: const _FailingExecutor()) - .select() - .asStream(); - - await expectLater(stream, emitsError(isA())); - }); - - test('a builder is a value: reuse does not leak steps', () async { - final executor = RecordingExecutor( - const PostgrestTableResult(data: >[]), - ); - final base = client.table(Books.table, executor: executor).select(); - - await base.where(Books.id.eq(1)); - await base.order(Books.title.asc()); - - expect(executor.requests[0].filter, isNotNull); - expect(executor.requests[0].orderings, isEmpty); - expect(executor.requests[1].filter, isNull); - expect(executor.requests[1].orderings, hasLength(1)); - }); - }); - - group('PostgrestHttpTableExecutor', () { - test('sends what the untyped builder sends for the same query', () async { - await client - .table(Books.table) - .selectOnly([Books.id, Books.title]) - .where(Books.id.gt(1) | Books.title.eq('a')) - .order(Books.title.desc().nullsLast()) - .range(10, 19); - await client - .from('books') - .select('id,title') - .or('id.gt.1,title.eq.a') - .order('title', ascending: false, nullsFirst: false) - .range(10, 19); - - final typed = httpClient.requests[0]; - final untyped = httpClient.requests[1]; - expect(typed.url.queryParameters, untyped.url.queryParameters); - expect(typed.method, untyped.method); - }); - - test('renders an offset without a limit', () async { - await PostgrestHttpTableExecutor(client).execute( - PostgrestTableRequest( - table: Books.table, - operation: PostgrestTableOperation.select, - offset: 5, - ), - ); - - final parameters = httpClient.requests.single.queryParameters; - expect(parameters['offset'], '5'); - expect(parameters.containsKey('limit'), isFalse); - }); - - test('renders an embedded offset without a limit', () async { - await PostgrestHttpTableExecutor(client).execute( - PostgrestTableRequest( - table: Books.table, - operation: PostgrestTableOperation.select, - embeddedPages: const [PostgrestEmbeddedPage('authors', offset: 3)], - ), - ); - - final parameters = httpClient.requests.single.queryParameters; - expect(parameters['authors.offset'], '3'); - expect(parameters.containsKey('authors.limit'), isFalse); - }); - - test('a request without a schema keeps the schema of the client', () async { - await PostgrestHttpTableExecutor(client.schema('library')).execute( - PostgrestTableRequest( - table: Books.table, - operation: PostgrestTableOperation.select, - ), - ); - - expect(httpClient.requests.single.headers['Accept-Profile'], 'library'); - }); - - test('rejects a filter on an insert instead of dropping it', () { - final request = PostgrestTableRequest( - table: Books.table, - operation: PostgrestTableOperation.insert, - payload: const {'title': 'c'}, - filter: Books.id.eq(1), - ); - - expect( - () => PostgrestHttpTableExecutor(client).execute(request), - throwsArgumentError, - ); - expect(httpClient.requests, isEmpty); - }); - - test('renders a returning select on a mutation', () async { - httpClient - ..reset() - ..stub([ - {'id': 3, 'title': 'c'}, - ]); - - final books = await client - .table(Books.table) - .insert(BookInsert(title: 'c')) - .selectOnly([Books.id]); - - final request = httpClient.requests.single; - expect(request.method, 'POST'); - expect(request.headers['Prefer'], contains('return=representation')); - expect(request.queryParameters['select'], 'id'); - expect(books.single.read(Books.id), 3); - }); - - test('scopes the request to the schema of the request', () async { - await client.schema('library').table(Books.table).select(); - - expect(httpClient.requests.single.headers['Accept-Profile'], 'library'); - }); - - test('renders a count only request as HEAD', () async { - httpClient - ..reset() - ..stub(null, headers: {'content-range': '0-1/2'}); - - final int count = await client.table(Books.table).count(); - - final request = httpClient.requests.single; - expect(request.method, 'HEAD'); - expect(request.headers['Prefer'], contains('count=exact')); - expect(count, 2); - }); - }); -} diff --git a/packages/postgrest/test/typed_query_test.dart b/packages/postgrest/test/typed_query_test.dart index bcc58fd98..af2e6da9c 100644 --- a/packages/postgrest/test/typed_query_test.dart +++ b/packages/postgrest/test/typed_query_test.dart @@ -974,7 +974,7 @@ void main() { expect(requestParameters()['id'], 'eq.1'); }); - test('update and delete start unscoped and keep the row type', () { + test('update and delete start unscoped and keep the row type', () async { final PostgrestTypedUnscopedBuilder update = client .table(Books.table) .update(BookUpdate(title: 'b')); @@ -987,9 +987,14 @@ void main() { ); final PostgrestTypedFilterBuilder scopedDelete = delete.all(); - expect(scopedUpdate.request.filter, isNotNull); - expect(scopedDelete.request.filter, isNull); expect(httpClient.requests, isEmpty); + + httpClient.stub(null); + await scopedUpdate; + await scopedDelete; + + expect(httpClient.requests.first.url.queryParameters['id'], 'eq.1'); + expect(httpClient.requests.last.url.queryParameters, isEmpty); }); test('all() updates every row without a filter', () async { diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index 54bf40936..fa9a8177e 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -1,6 +1,3 @@ -// The typed table executor this client hands its plugins is @experimental. -// ignore_for_file: experimental_member_use - import 'dart:async'; import 'package:http/http.dart'; @@ -56,10 +53,6 @@ import 'trace_http_client.dart'; /// parser. A codec passed here is owned by the caller, so [dispose] leaves it /// alone, and it can be shared with other clients. /// -/// [plugins] extend the client from the outside, see [SupabaseClientPlugin]. -/// They wrap the executor the typed table API runs through, in the order -/// given, and are disposed together with the client. -/// /// The pkce flow is used by default and keeps its code verifiers in the /// `AuthAsyncStorage` passed to the `asyncStorage` field of [authOptions]. /// Pass a persistent implementation whenever the flow can leave the process @@ -83,11 +76,7 @@ class SupabaseClient { Map? headers, Client? httpClient, AsyncJsonCodec? jsonCodec, - List plugins = const [], }) : _supabaseKey = supabaseKey, - // Disposed one by one from dispose, which the rule cannot see. - // ignore: dispose-class-fields - plugins = List.unmodifiable(plugins), _functionsOptions = functionsOptions, _restUrl = '$supabaseUrl/rest/v1', _realtimeUrl = '$supabaseUrl/realtime/v1'.replaceAll('http', 'ws'), @@ -141,20 +130,6 @@ class SupabaseClient { 'Initialize SupabaseClient v$version with custom access token', ); } - try { - _tableExecutor = plugins.fold( - _RestTableExecutor(this), - (executor, plugin) => plugin.wrapTableExecutor(executor), - ); - for (final plugin in plugins) { - plugin.attach(this); - } - } catch (_) { - // The caller never receives the client, so nothing else can free what - // the constructor already opened. - unawaited(_disposeOwnResources()); - rethrow; - } } final String _supabaseKey; final PostgrestClientOptions _postgrestOptions; @@ -184,13 +159,6 @@ class SupabaseClient { /// broadcast messages between clients. late final RealtimeClient realtime; late PostgrestClient _rest; - late final PostgrestTableExecutor _tableExecutor; - - /// The plugins given to the constructor, in the order they wrap the typed - /// table executor. - /// - /// Each plugin is disposed from [dispose]. - final List plugins; StreamSubscription? _authStateSubscription; final AsyncJsonCodec _jsonCodec; final bool _ownsJsonCodec; @@ -275,7 +243,6 @@ class SupabaseClient { authHttpClient: _authHttpClient, realtime: realtime, rest: rest, - tableExecutor: _tableExecutor, ); /// Perform a table operation. @@ -371,21 +338,6 @@ class SupabaseClient { /// the functions, PostgREST, and its internally owned JSON codec. Future dispose() async { clientLogger.fine('Dispose SupabaseClient'); - for (final plugin in plugins.reversed) { - try { - await plugin.dispose(); - } catch (error, stackTrace) { - clientLogger.warning( - 'Plugin ${plugin.runtimeType} failed to dispose', - error, - stackTrace, - ); - } - } - await _disposeOwnResources(); - } - - Future _disposeOwnResources() async { await realtime.disconnect(); await _authStateSubscription?.cancel(); await functions.dispose(); @@ -525,16 +477,3 @@ class SupabaseClient { } } } - -/// Sends typed table requests through whichever rest client the -/// [SupabaseClient] currently holds, so a header change that replaces the -/// rest client does not require the plugin chain to be rebuilt. -class _RestTableExecutor implements PostgrestTableExecutor { - const _RestTableExecutor(this._client); - - final SupabaseClient _client; - - @override - Future execute(PostgrestTableRequest request) => - PostgrestHttpTableExecutor(_client._rest).execute(request); -} diff --git a/packages/supabase/lib/src/supabase_client_plugin.dart b/packages/supabase/lib/src/supabase_client_plugin.dart deleted file mode 100644 index 67bc12d8e..000000000 --- a/packages/supabase/lib/src/supabase_client_plugin.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:postgrest/postgrest.dart'; - -import 'supabase_client.dart'; - -/// Extends a [SupabaseClient] from the outside, without changing how the -/// application calls it. -/// -/// A plugin can wrap the [PostgrestTableExecutor] that runs the requests of -/// the typed table API, [SupabaseClient.table], and receives the client once -/// it is fully constructed. The untyped [SupabaseClient.from], `rpc`, -/// storage, functions and realtime traffic never passes through a plugin. -/// -/// Plugins are given to the [SupabaseClient] constructor, or to -/// `Supabase.initialize` in Flutter: -/// -/// ```dart -/// final supabase = SupabaseClient(url, key, plugins: [offline]); -/// ``` -@experimental -abstract class SupabaseClientPlugin { - const SupabaseClientPlugin(); - - /// Returns the executor [SupabaseClient.table] runs its requests through. - /// - /// [inner] sends a request to PostgREST with the current access token, so - /// an executor that holds a request back and runs it through [inner] later - /// still authenticates as the current user. Plugins wrap in the order they - /// were given, so the last plugin sees a request first. Return [inner] to - /// leave execution untouched. - PostgrestTableExecutor wrapTableExecutor(PostgrestTableExecutor inner) => - inner; - - /// Called once with the fully constructed [client], after every service - /// client exists. - void attach(SupabaseClient client) {} - - /// Called when the host application returns to the foreground, for - /// example on `AppLifecycleState.resumed` in Flutter. - /// - /// The host does not wait for the returned future, so a long running - /// reaction, a synchronization for example, does not delay anything else - /// the host does on resume. An error the future completes with is logged. - Future resume() async {} - - /// Called from [SupabaseClient.dispose] before the service clients are - /// disposed. An error thrown here is logged and does not stop the other - /// plugins or the client from being disposed. - Future dispose() async {} -} diff --git a/packages/supabase/lib/src/supabase_query_schema.dart b/packages/supabase/lib/src/supabase_query_schema.dart index d0cb6361b..3d24f48dc 100644 --- a/packages/supabase/lib/src/supabase_query_schema.dart +++ b/packages/supabase/lib/src/supabase_query_schema.dart @@ -1,6 +1,3 @@ -// The typed table executor is @experimental. -// ignore_for_file: experimental_member_use - import 'package:http/http.dart'; import 'package:meta/meta.dart'; import 'package:supabase/supabase.dart'; @@ -10,7 +7,7 @@ import 'counter.dart'; /// Used to perform [rpc] and [from] operations with a different schema than in /// [SupabaseClient]. class SupabaseQuerySchema { - SupabaseQuerySchema({ + const SupabaseQuerySchema({ required Counter counter, required String restUrl, required String schema, @@ -18,15 +15,13 @@ class SupabaseQuerySchema { required Client? authHttpClient, required RealtimeClient realtime, required PostgrestClient rest, - PostgrestTableExecutor? tableExecutor, }) : _counter = counter, _restUrl = restUrl, _schema = schema, _jsonCodec = jsonCodec, _authHttpClient = authHttpClient, _realtime = realtime, - _rest = rest, - _tableExecutor = tableExecutor ?? PostgrestHttpTableExecutor(rest); + _rest = rest; final Counter _counter; final String _restUrl; final String _schema; @@ -34,7 +29,6 @@ class SupabaseQuerySchema { final Client? _authHttpClient; final RealtimeClient _realtime; final PostgrestClient _rest; - final PostgrestTableExecutor _tableExecutor; /// Perform a table operation. SupabaseQueryBuilder from(String table) { @@ -63,7 +57,7 @@ class SupabaseQuerySchema { return SupabaseTypedQueryBuilder( from(table.name), table, - executor: _tableExecutor, + client: _rest, schema: _schema, ); } @@ -92,7 +86,6 @@ class SupabaseQuerySchema { authHttpClient: _authHttpClient, realtime: _realtime, rest: newRest, - tableExecutor: _tableExecutor, ); } } diff --git a/packages/supabase/lib/src/supabase_typed_query_builder.dart b/packages/supabase/lib/src/supabase_typed_query_builder.dart index 13031df8d..7a529c260 100644 --- a/packages/supabase/lib/src/supabase_typed_query_builder.dart +++ b/packages/supabase/lib/src/supabase_typed_query_builder.dart @@ -13,13 +13,13 @@ class SupabaseTypedQueryBuilder // The query builder is also kept as a field to expose [stream], so it // cannot become a super parameter. // ignore: use_super_parameters - const SupabaseTypedQueryBuilder( + SupabaseTypedQueryBuilder( SupabaseQueryBuilder queryBuilder, PostgrestTable table, { - required PostgrestTableExecutor executor, + required PostgrestClient client, String? schema, }) : _queryBuilder = queryBuilder, - super(table, executor: executor, schema: schema); + super(table, client: client, schema: schema); final SupabaseQueryBuilder _queryBuilder; diff --git a/packages/supabase/lib/supabase.dart b/packages/supabase/lib/supabase.dart index 15025ba79..7969e8539 100644 --- a/packages/supabase/lib/supabase.dart +++ b/packages/supabase/lib/supabase.dart @@ -12,7 +12,6 @@ export 'package:supabase_storage/supabase_storage.dart'; export 'src/realtime_client_options.dart'; export 'src/supabase_client.dart'; export 'src/supabase_client_options.dart'; -export 'src/supabase_client_plugin.dart'; export 'src/supabase_query_builder.dart'; export 'src/supabase_query_schema.dart'; export 'src/supabase_stream_builder.dart'; diff --git a/packages/supabase/test/client_plugin_test.dart b/packages/supabase/test/client_plugin_test.dart deleted file mode 100644 index fc77b2d1a..000000000 --- a/packages/supabase/test/client_plugin_test.dart +++ /dev/null @@ -1,199 +0,0 @@ -// The typed table access API and the plugin seam are @experimental. -// ignore_for_file: experimental_member_use - -import 'package:supabase/supabase.dart'; -import 'package:test/test.dart'; - -import 'utils.dart'; - -extension type const Todo(Map _json) - implements Map { - int get id => _json['id'] as int; - String get task => _json['task'] as String; -} - -extension type const TodoInsert._(Map _json) - implements Object { - TodoInsert({required String task}) : this._({'task': task}); -} - -extension type const TodoUpdate._(Map _json) - implements Object { - TodoUpdate({String? task}) : this._({'task': ?task}); -} - -class Todos { - static const table = PostgrestTable( - 'todos', - Todo.new, - primaryKey: [id], - ); - static const id = PostgrestColumn('id'); - static const task = PostgrestColumn('task'); -} - -const todoRows = [ - {'id': 1, 'task': 'Ship it'}, -]; - -class LoggingExecutor implements PostgrestTableExecutor { - const LoggingExecutor(this.name, this.inner, this.log); - - final String name; - final PostgrestTableExecutor inner; - final List log; - - @override - Future execute(PostgrestTableRequest request) { - log.add('$name ${request.operation.name} ${request.table.name}'); - return inner.execute(request); - } -} - -class LoggingPlugin extends SupabaseClientPlugin { - LoggingPlugin(this.name, this.log); - - final String name; - final List log; - SupabaseClient? attachedClient; - int disposals = 0; - - @override - PostgrestTableExecutor wrapTableExecutor(PostgrestTableExecutor inner) => - LoggingExecutor(name, inner, log); - - @override - void attach(SupabaseClient client) { - attachedClient = client; - log.add('$name attached'); - } - - @override - Future dispose() async { - disposals++; - log.add('$name disposed'); - } -} - -class FailingAttachPlugin extends SupabaseClientPlugin { - const FailingAttachPlugin(); - - @override - void attach(SupabaseClient client) => throw StateError('no attach'); -} - -void main() { - late MockSupabaseHttpClient httpClient; - late List log; - late LoggingPlugin plugin; - late SupabaseClient supabase; - - setUp(() { - httpClient = MockSupabaseHttpClient()..stubTable('todos', rows: todoRows); - log = []; - plugin = LoggingPlugin('a', log); - supabase = testSupabaseClient(httpClient: httpClient, plugins: [plugin]); - }); - - tearDown(() async { - await supabase.dispose(); - }); - - test('table() runs through the plugin, from() does not', () async { - final List typed = await supabase.table(Todos.table).select(); - await supabase.from('todos').select(); - - expect(typed.single.task, 'Ship it'); - expect(log, ['a attached', 'a select todos']); - expect(httpClient.requestsTo('/rest/v1/todos'), hasLength(2)); - }); - - test('rpc, storage and functions bypass the plugin', () async { - httpClient - ..stubRpc('ping', body: 'pong') - ..stubStorageList('avatars', objects: []) - ..stubEdgeFunction('hello', body: {'ok': true}); - - await supabase.rpc('ping'); - await supabase.storage.from('avatars').list(); - await supabase.functions.invoke('hello'); - - expect(log, ['a attached']); - }); - - test( - 'the wire carries the session while the request value does not', - () async { - final session = await signInTestUser(supabase.auth); - - await supabase.table(Todos.table).select(); - - final sent = httpClient.requestsTo('/rest/v1/todos').single; - expect(sent.headers['Authorization'], 'Bearer ${session.accessToken}'); - expect(sent.headers['apikey'], isNotNull); - }, - ); - - test('attach receives the constructed client', () { - expect(plugin.attachedClient, same(supabase)); - expect(plugin.attachedClient!.rest.url, endsWith('/rest/v1')); - }); - - test('plugins wrap in order and dispose in reverse', () async { - await supabase.dispose(); - log.clear(); - final first = LoggingPlugin('first', log); - final second = LoggingPlugin('second', log); - supabase = testSupabaseClient( - httpClient: httpClient, - plugins: [first, second], - ); - - await supabase.table(Todos.table).select(); - await supabase.dispose(); - - expect(log, [ - 'first attached', - 'second attached', - 'second select todos', - 'first select todos', - 'second disposed', - 'first disposed', - ]); - expect(supabase.plugins, [first, second]); - }); - - test('a header change keeps the plugin chain in place', () async { - supabase.headers = {'x-tenant': 'acme'}; - - await supabase.table(Todos.table).select(); - - expect(log, ['a attached', 'a select todos']); - final sent = httpClient.requestsTo('/rest/v1/todos').single; - expect(sent.headers['x-tenant'], 'acme'); - }); - - test('a schema scoped table request carries its schema', () async { - await supabase.schema('library').table(Todos.table).select(); - - final sent = httpClient.requestsTo('/rest/v1/todos').single; - expect(sent.headers['Accept-Profile'], 'library'); - expect(log, ['a attached', 'a select todos']); - }); - - test('a plugin that fails to attach fails the constructor', () { - expect( - () => testSupabaseClient( - httpClient: httpClient, - plugins: const [FailingAttachPlugin()], - ), - throwsStateError, - ); - }); - - test('dispose runs each plugin once', () async { - await supabase.dispose(); - - expect(plugin.disposals, 1); - }); -} diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index 3e07aa99f..403f21556 100644 --- a/packages/supabase_flutter/lib/src/supabase.dart +++ b/packages/supabase_flutter/lib/src/supabase.dart @@ -1,6 +1,3 @@ -// The plugin seam is @experimental. -// ignore_for_file: experimental_member_use - import 'dart:async'; import 'package:flutter/foundation.dart'; @@ -95,10 +92,6 @@ class Supabase { /// [AuthFlowType.implicit] to use the old implicit flow for authentication /// involving deep links. /// - /// [plugins] extend the client from the outside, see - /// [SupabaseClientPlugin]. Each plugin is resumed when the app returns to - /// the foreground and disposed together with the client. - /// /// All Supabase packages log through `package:logging` using loggers under /// the `supabase` hierarchy (for example `supabase.auth` or /// `supabase.realtime`). Nothing is printed by default; attach a listener @@ -118,7 +111,6 @@ class Supabase { TracePropagationOptions? tracePropagationOptions, Future Function()? accessToken, AsyncJsonCodec? jsonCodec, - List plugins = const [], }) async { if (_instance._isInitialized) { flutterLogger.info( @@ -148,7 +140,6 @@ class Supabase { tracePropagationOptions: tracePropagationOptions, accessToken: accessToken, jsonCodec: jsonCodec, - plugins: plugins, ); if (accessToken == null) { @@ -226,11 +217,6 @@ class Supabase { /// appends via `.then()` so operations never overlap. Future _pendingLifecycleOperation = Future.value(); - /// The resume work of each plugin, one chain per plugin so a resume never - /// overlaps the previous one of the same plugin. [dispose] waits for the - /// chains before disposing the client. - final _pluginResumes = >{}; - /// The most recently requested lifecycle state. Checked inside /// [_processLifecycle] after each `await` to skip stale operations /// (e.g. abort a reconnect if the app went back to background). @@ -247,8 +233,6 @@ class Supabase { final supabaseAuth = _supabaseAuth; final lifecycleListener = _lifecycleListener; final pendingLifecycleOperation = _pendingLifecycleOperation; - final pluginResumes = _pluginResumes.values.toList(); - _pluginResumes.clear(); _client = null; _supabaseAuth = null; @@ -265,7 +249,6 @@ class Supabase { await _disposeAll([ () => supabaseAuth?.dispose(), () => pendingLifecycleOperation, - () => Future.wait(pluginResumes), currentClient.dispose, ]); } @@ -307,7 +290,6 @@ class Supabase { required TracePropagationOptions? tracePropagationOptions, required Future Function()? accessToken, required AsyncJsonCodec? jsonCodec, - required List plugins, }) { _realtimeLifecycleOptions = realtimeLifecycleOptions; final headers = { @@ -326,7 +308,6 @@ class Supabase { tracePropagationOptions: tracePropagationOptions, accessToken: accessToken, jsonCodec: jsonCodec, - plugins: plugins, ); // Close any previous realtime client that may still be connected due to @@ -417,19 +398,6 @@ class Supabase { final realtime = currentClient.realtime; if (captured == AppLifecycleState.resumed) { - for (final plugin in currentClient.plugins) { - final previous = _pluginResumes[plugin] ?? Future.value(); - _pluginResumes[plugin] = previous - .then((_) => Future.sync(plugin.resume)) - .catchError((Object error, StackTrace stackTrace) { - flutterLogger.warning( - 'Plugin ${plugin.runtimeType} failed to resume', - error, - stackTrace, - ); - }); - } - if (!_realtimeLifecycleOptions.managed) return; // No channels subscribed — nothing to reconnect. diff --git a/packages/supabase_flutter/test/plugin_test.dart b/packages/supabase_flutter/test/plugin_test.dart deleted file mode 100644 index ae52bf017..000000000 --- a/packages/supabase_flutter/test/plugin_test.dart +++ /dev/null @@ -1,143 +0,0 @@ -// The plugin seam is @experimental. -// ignore_for_file: experimental_member_use - -import 'dart:async'; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -import 'utils.dart'; -import 'widget_test_stubs.dart'; - -class CountingPlugin extends SupabaseClientPlugin { - SupabaseClient? attachedClient; - int resumes = 0; - int disposals = 0; - bool failNextResume = false; - Completer? gate; - int finishedResumes = 0; - - @override - void attach(SupabaseClient client) { - attachedClient = client; - } - - @override - Future resume() async { - resumes++; - if (failNextResume) { - failNextResume = false; - throw StateError('resume failed'); - } - await gate?.future; - finishedResumes++; - } - - @override - Future dispose() async { - disposals++; - } -} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - late CountingPlugin plugin; - - setUp(() async { - mockSharedPreferences(); - mockAppLink(); - plugin = CountingPlugin(); - await Supabase.initialize( - url: '', - publishableKey: '', - authOptions: FlutterAuthClientOptions(asyncStorage: MockAsyncStorage()), - plugins: [plugin], - ); - }); - - tearDown(() async { - try { - await Supabase.instance.dispose(); - } catch (_) {} - }); - - /// Sends the app to the background and brings it back. - Future cycleToResumed() async { - final binding = TestWidgetsFlutterBinding.instance; - binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); - binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden); - binding.handleAppLifecycleStateChanged(AppLifecycleState.paused); - binding.handleAppLifecycleStateChanged(AppLifecycleState.detached); - binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); - await pumpEventQueue(); - } - - test('plugins reach the client and are attached', () { - expect(Supabase.instance.client.plugins, [plugin]); - expect(plugin.attachedClient, same(Supabase.instance.client)); - }); - - test('resume runs on resumed even without realtime channels', () async { - expect(Supabase.instance.client.realtime.channels, isEmpty); - - await cycleToResumed(); - - expect(plugin.resumes, 1); - }); - - test('a failing resume is logged and later resumes still run', () async { - plugin.failNextResume = true; - - await cycleToResumed(); - await cycleToResumed(); - - expect(plugin.resumes, 2); - }); - - test('resumes of one plugin run one at a time', () async { - final gate = Completer(); - plugin.gate = gate; - - await cycleToResumed(); - await cycleToResumed(); - - expect(plugin.resumes, 1); - - gate.complete(); - plugin.gate = null; - await pumpEventQueue(); - - expect(plugin.resumes, 2); - expect(plugin.finishedResumes, 2); - }); - - test('dispose waits for a running resume', () async { - final gate = Completer(); - plugin.gate = gate; - await cycleToResumed(); - - var disposed = false; - final disposing = Supabase.instance.dispose().then((_) => disposed = true); - await pumpEventQueue(); - - expect(disposed, isFalse); - expect(plugin.disposals, 0); - - gate.complete(); - await disposing; - - expect(plugin.finishedResumes, 1); - expect(plugin.disposals, 1); - }); - - test('dispose runs the plugin and stops resume events', () async { - await Supabase.instance.dispose(); - - await cycleToResumed(); - - expect(plugin.disposals, 1); - expect(plugin.resumes, 0); - }); -} diff --git a/packages/supabase_test/lib/src/test_supabase_client.dart b/packages/supabase_test/lib/src/test_supabase_client.dart index b7f817f35..829c21711 100644 --- a/packages/supabase_test/lib/src/test_supabase_client.dart +++ b/packages/supabase_test/lib/src/test_supabase_client.dart @@ -41,8 +41,6 @@ SupabaseClient testSupabaseClient({ String url = 'http://localhost:54321', String? apiKey, bool autoRefreshToken = false, - // ignore: experimental_member_use - List plugins = const [], }) { return SupabaseClient( url, @@ -53,7 +51,6 @@ SupabaseClient testSupabaseClient({ asyncStorage: MemoryAuthAsyncStorage(), ), realtimeClientOptions: RealtimeClientOptions(transport: realtime?.call), - plugins: plugins, ); } diff --git a/sdk-compliance.yaml b/sdk-compliance.yaml index 6a8eb78df..f5a11070a 100644 --- a/sdk-compliance.yaml +++ b/sdk-compliance.yaml @@ -814,65 +814,6 @@ features: - PostgrestTable.relations - PostgrestTable.computedFields - RowConverter - - PostgrestTypedBuilder.request - - PostgrestTableRequest - - PostgrestTableRequest.PostgrestTableRequest - - PostgrestTableRequest.table - - PostgrestTableRequest.operation - - PostgrestTableRequest.schema - - PostgrestTableRequest.columns - - PostgrestTableRequest.filter - - PostgrestTableRequest.orderings - - PostgrestTableRequest.limit - - PostgrestTableRequest.offset - - PostgrestTableRequest.embeddedPages - - PostgrestTableRequest.shape - - PostgrestTableRequest.countOption - - PostgrestTableRequest.payload - - PostgrestTableRequest.onConflict - - PostgrestTableRequest.ignoreDuplicates - - PostgrestTableRequest.defaultToNull - - PostgrestTableRequest.maxAffected - - PostgrestTableRequest.stripNulls - - PostgrestTableRequest.dryRun - - PostgrestTableRequest.explainOptions - - PostgrestTableRequest.returning - - PostgrestTableRequest.copyWith - - PostgrestTableRequest.options - - PostgrestTableOperation - - PostgrestTableOperation.isMutation - - PostgrestResultShape - - PostgrestEmbeddedPage - - PostgrestEmbeddedPage.PostgrestEmbeddedPage - - PostgrestEmbeddedPage.referencedTable - - PostgrestEmbeddedPage.limit - - PostgrestEmbeddedPage.offset - - PostgrestExplainOptions - - PostgrestExplainOptions.PostgrestExplainOptions - - PostgrestExplainOptions.analyze - - PostgrestExplainOptions.verbose - - PostgrestExplainOptions.settings - - PostgrestExplainOptions.buffers - - PostgrestExplainOptions.wal - - PostgrestExplainOptions.format - - PostgrestRequestOptions - - PostgrestRequestOptions.PostgrestRequestOptions - - PostgrestRequestOptions.headers - - PostgrestRequestOptions.retryEnabled - - PostgrestRequestOptions.retryCount - - PostgrestRequestOptions.requestTimeout - - PostgrestRequestOptions.abortSignal - - PostgrestRequestOptions.copyWith - - PostgrestTableResult - - PostgrestTableResult.PostgrestTableResult - - PostgrestTableResult.data - - PostgrestTableResult.count - - PostgrestTableExecutor - - PostgrestTableExecutor.execute - - PostgrestHttpTableExecutor - - PostgrestHttpTableExecutor.PostgrestHttpTableExecutor - - PostgrestHttpTableExecutor.client - - PostgrestHttpTableExecutor.execute - PostgrestSelectable - PostgrestSelectable.expression - PostgrestSelectable.responseKey @@ -2684,14 +2625,6 @@ features: symbols: - PostgrestClient.httpClient - RealtimeClient.httpClient - supporting_symbols: - - SupabaseClient.plugins - - SupabaseClientPlugin - - SupabaseClientPlugin.SupabaseClientPlugin - - SupabaseClientPlugin.wrapTableExecutor - - SupabaseClientPlugin.attach - - SupabaseClientPlugin.resume - - SupabaseClientPlugin.dispose client.request_configuration.global_headers: status: implemented symbols: