Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,25 @@ await supabase.from('countries');
await supabase.from('countries').select();
```

### `rpc` is a getter

`PostgrestClient.rpc`, `SupabaseClient.rpc` and `SupabaseQuerySchema.rpc` are getters returning a
`PostgrestRpc` instead of methods. Calling the result makes the same untyped call as before, so
`supabase.rpc('fn', params: {...})` and `supabase.rpc<int>('fn')` are unchanged. The extension
methods `supabase_typegen` generates on `PostgrestRpc` are what make
`supabase.rpc.searchBooks(query: 'dart')` typed.

Code that passes `rpc` itself around as a function no longer compiles, since the getter returns an
object rather than a function. Wrap it in a closure instead.

```dart
// Before
final call = supabase.rpc;

// After
PostgrestFilterBuilder<dynamic> call(String fn) => supabase.rpc(fn);
```

### `insert()` and `upsert()` no longer take filters

`insert()` and `upsert()` return a `PostgrestTransformBuilder` instead of a
Expand Down
36 changes: 21 additions & 15 deletions packages/postgrest/lib/src/postgrest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -190,31 +190,37 @@ class PostgrestClient {
);
}

/// {@template postgrest_rpc}
/// Performs a stored procedure call.
/// Calls Postgres functions in the schema of this client.
///
/// [fn] is the name of the function to call.
///
/// [params] is an optional object to pass as arguments to the function call.
///
/// When [get] is set to `true`, the function will be called with read-only
/// access mode.
///
/// {@endtemplate}
/// Calling the result performs an untyped call, see [PostgrestRpc.call]:
///
/// ```dart
/// supabase.rpc('get_status', params: {'name_param': 'supabot'})
/// client.rpc('get_status', params: {'name_param': 'supabot'})
/// ```
PostgrestFilterBuilder<T> rpc<T>(
String fn, {
Map<dynamic, dynamic>? params,
///
/// The extension methods `package:supabase_typegen` generates on
/// [PostgrestRpc] call the functions of the database with typed arguments
/// and results, `client.rpc.searchBooks(query: 'dart')`.
PostgrestRpc get rpc => PostgrestRpc(
this,
schema: _schema,
schemaIsExplicit: _schemaIsExplicit,
);

/// Like [rpc], but with the call scoped to [schema] instead of the schema
/// this client was created with; `null` keeps the client's schema.
@internal
PostgrestFilterBuilder<T> rpcInSchema<T>(
String fn,
String? schema, {
Object? params,
bool get = false,
}) {
final requestUrl = '$url/rpc/$fn';
return PostgrestRpcBuilder(
requestUrl,
headers: headers,
schema: _schema,
schema: schema ?? _schema,
httpClient: httpClient,
jsonCodec: _jsonCodec,
retryOptions: retryOptions,
Expand Down
144 changes: 144 additions & 0 deletions packages/postgrest/lib/src/postgrest_rpc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
part of 'postgrest_typed_builder.dart';

/// Calls Postgres functions, returned by [PostgrestClient.rpc].
///
/// Calling it performs an untyped call, whose result is the decoded response
/// body:
///
/// ```dart
/// final status = await client.rpc('get_status', params: {'name': 'supabot'});
/// ```
///
/// `package:supabase_typegen` generates an extension method on this class for
/// every function of the database, which takes the arguments as typed
/// parameters and resolves to typed results:
///
/// ```dart
/// final List<BooksRow> books = await client.rpc
/// .searchBooks(query: 'dart')
/// .where(Books.publishedOn.isNull().not())
/// .order(Books.title.asc());
/// ```
///
/// The generated methods are built on [rows] and [value].
final class PostgrestRpc {
/// Calls functions through [client], in [schema] or the schema of the
/// client when `null`. When [schemaIsExplicit] is set, [schema] also wins
/// over the schema a generated method passes along.
@internal
const PostgrestRpc(
this._client, {
String? schema,
bool schemaIsExplicit = false,
}) : _schema = schema,
_schemaIsExplicit = schemaIsExplicit;

final PostgrestClient _client;
final String? _schema;
final bool _schemaIsExplicit;

/// {@template postgrest_rpc}
/// Performs a stored procedure call.
///
/// [fn] is the name of the function to call.
///
/// [params] is an optional object to pass as arguments to the function call.
///
/// When [get] is set to `true`, the function will be called with read-only
/// access mode.
/// {@endtemplate}
///
/// ```dart
/// client.rpc('get_status', params: {'name_param': 'supabot'})
/// ```
PostgrestFilterBuilder<T> call<T>(
String fn, {
Map<dynamic, dynamic>? params,
bool get = false,
}) =>
// ignore: invalid_use_of_internal_member
_client.rpcInSchema(fn, _schema, params: params, get: get);

/// Calls [function] with [arguments] and reads the rows it returns through
/// [rowFromJson].
///
/// The result can be filtered, ordered and paged like the rows of a table,
/// with the column tokens of the row type:
///
/// ```dart
/// final List<BooksRow> books = await client.rpc
/// .rows(
/// 'search_books',
/// arguments: {'query': 'dart'},
/// rowFromJson: BooksRow.new,
/// )
/// .where(Books.id.gt(10));
/// ```
///
/// [arguments] is sent as the request body: the JSON object of named
/// arguments, or the value of a function's sole unnamed `json` or `jsonb`
/// parameter. The call addresses [schema] unless this object was scoped
/// with [PostgrestClient.schema].
@experimental
PostgrestTypedFilterBuilder<Row, List<Row>> rows<Row>(
String function, {
Object? arguments,
required RowConverter<Row> rowFromJson,
String? schema,
}) => PostgrestTypedFilterBuilder._(
_request(function, arguments, rowFromJson, schema),
PostgrestHttpTableExecutor(_client),
(result) => [
for (final row in result.data! as List<dynamic>)
rowFromJson(row as Map<String, dynamic>),
],
rowFromJson,
);

/// Calls [function] with [arguments] and converts the response body, a
/// scalar, a single row or a list of scalars, through [fromJson].
///
/// ```dart
/// final int? count = await client.rpc.value(
/// 'count_books',
/// fromJson: (json) => json as int?,
/// );
/// ```
///
/// See [rows] for [arguments] and [schema].
@experimental
PostgrestTypedBuilder<T> value<T>(
String function, {
Object? arguments,
required T Function(Object? json) fromJson,
String? schema,
}) => PostgrestTypedBuilder._(
_request(
function,
arguments,
_noRows,
schema,
).copyWith(shape: PostgrestResultShape.value),
PostgrestHttpTableExecutor(_client),
(result) => fromJson(result.data),
);

PostgrestTableRequest _request<Row>(
String function,
Object? arguments,
RowConverter<Row> rowFromJson,
String? schema,
) => PostgrestTableRequest(
table: PostgrestTable<Row, Never, Never>(
function,
rowFromJson,
primaryKey: const [],
),
operation: PostgrestTableOperation.rpc,
schema: _schemaIsExplicit ? _schema : (schema ?? _schema),
payload: arguments,
);
}

Never _noRows(Map<String, dynamic> json) =>
throw StateError('A function call read as a value has no rows');
61 changes: 46 additions & 15 deletions packages/postgrest/lib/src/postgrest_table_executor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,10 @@ final class PostgrestHttpTableExecutor implements PostgrestTableExecutor {

@override
Future<PostgrestTableResult> execute(PostgrestTableRequest request) async {
// ignore: invalid_use_of_internal_member
final query = client.fromInSchema(request.table.name, request.schema);

if (request.operation == PostgrestTableOperation.count) {
final counted = _applyOptions(
_applyFilter(
query.count(request.countOption ?? CountOption.exact),
_query(request).count(request.countOption ?? CountOption.exact),
request,
),
request.options,
Expand All @@ -53,7 +50,7 @@ final class PostgrestHttpTableExecutor implements PostgrestTableExecutor {
}

final terminal = _applyOptions(
_terminal(_transforms(_operation(query, request), request), request),
_terminal(_transforms(_operation(request), request), request),
request.options,
);

Expand Down Expand Up @@ -97,42 +94,74 @@ final class PostgrestHttpTableExecutor implements PostgrestTableExecutor {
/// An insert or upsert takes no filter, the same way the untyped builders
/// offer none after them; a hand-built request that carries one is
/// rejected rather than sent with the filter silently dropped.
static PostgrestTransformBuilder<Object?> _operation(
PostgrestQueryBuilder query,
PostgrestTableRequest request,
) {
PostgrestTransformBuilder<Object?> _operation(PostgrestTableRequest request) {
final filter = request.filter;
switch (request.operation) {
case PostgrestTableOperation.select:
return _applyFilter(
query.select(_selectList(request.columns)),
_query(request).select(_selectList(request.columns)),
request,
);
case PostgrestTableOperation.update:
return _applyFilter(query.update(request.payload!), request);
return _applyFilter(_query(request).update(request.payload!), request);
case PostgrestTableOperation.delete:
return _applyFilter(query.delete(), request);
return _applyFilter(_query(request).delete(), request);
case PostgrestTableOperation.insert:
_rejectFilter(filter, request.operation);
return query.insert(
return _query(request).insert(
request.payload!,
defaultToNull: request.defaultToNull,
);
case PostgrestTableOperation.upsert:
_rejectFilter(filter, request.operation);
return query.upsert(
return _query(request).upsert(
request.payload!,
onConflict: request.onConflict
?.map((column) => column.name)
.join(','),
ignoreDuplicates: request.ignoreDuplicates,
defaultToNull: request.defaultToNull,
);
case PostgrestTableOperation.rpc:
return _applyFilter(_call(request), request);
case PostgrestTableOperation.count:
throw StateError('count is rendered before the other operations');
}
}

/// The table builder of [request], in its schema.
PostgrestQueryBuilder _query(PostgrestTableRequest request) =>
// ignore: invalid_use_of_internal_member
client.fromInSchema(request.table.name, request.schema);

/// The function call of [request], decoding a response read as rows into
/// a [PostgrestList] the way a select does.
///
/// A HEAD request has no body, so a call read with
/// [PostgrestResultShape.head] sends its arguments as query parameters.
PostgrestFilterBuilder<Object?> _call(PostgrestTableRequest request) {
final name = request.table.name;
final head = request.shape == PostgrestResultShape.head;
final arguments = head
? (request.payload ?? const <String, Object?>{})
: request.payload;
if (request.shape == PostgrestResultShape.value) {
// ignore: invalid_use_of_internal_member
return client.rpcInSchema(
name,
request.schema,
params: arguments,
);
}
// ignore: invalid_use_of_internal_member
return client.rpcInSchema<PostgrestList>(
name,
request.schema,
params: arguments,
get: head,
);
}

static void _rejectFilter(
PostgrestFilter<Object?>? filter,
PostgrestTableOperation operation,
Expand Down Expand Up @@ -218,7 +247,9 @@ final class PostgrestHttpTableExecutor implements PostgrestTableExecutor {
PostgrestTransformBuilder<Object?> transformed,
PostgrestTableRequest request,
) => switch (request.shape) {
PostgrestResultShape.rows || PostgrestResultShape.none => transformed,
PostgrestResultShape.rows ||
PostgrestResultShape.none ||
PostgrestResultShape.value => transformed,
PostgrestResultShape.single => transformed.single(),
PostgrestResultShape.maybeSingle => transformed.maybeSingle(),
PostgrestResultShape.head => transformed.head(),
Expand Down
Loading
Loading