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
4 changes: 3 additions & 1 deletion packages/postgrest/lib/src/postgrest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ class PostgrestClient {
/// .where(Books.id.gt(10));
/// ```
@experimental
PostgrestTypedQueryBuilder<Row> table<Row>(PostgrestTable<Row> table) {
PostgrestTypedQueryBuilder<Row, Insert, Update> table<Row, Insert, Update>(
PostgrestTable<Row, Insert, Update> table,
) {
return PostgrestTypedQueryBuilder(from(table.name), table);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/postgrest/lib/src/postgrest_query_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ class PostgrestQueryBuilder {
/// .eq('message', 'foo')
/// .select();
/// ```
PostgrestFilterBuilder<void> update(Map<dynamic, dynamic> values) {
PostgrestFilterBuilder<void> update(Object values) {
final newHeaders = {..._config.headers}..remove('Prefer');

return _filterBuilder(
Expand Down
37 changes: 30 additions & 7 deletions packages/postgrest/lib/src/postgrest_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,32 @@ part of 'postgrest_typed_builder.dart';
@experimental
typedef RowConverter<Row> = Row Function(Map<String, dynamic> json);

/// Describes a database table (or view) together with the Dart type its rows
/// are converted into.
/// Describes a database table (or view) together with the Dart types its rows
/// are read as and written with.
///
/// Passing a [PostgrestTable] to [PostgrestClient.table] gives fully typed
/// query results, so no raw `Map<String, dynamic>` needs to be handled:
/// query results, so no raw `Map<String, dynamic>` needs to be handled, and
/// only accepts [Insert] and [Update] values on the write methods:
///
/// ```dart
/// extension type Book(Map<String, dynamic> json) {
/// extension type Book(Map<String, dynamic> json) implements Object {
/// int get id => json['id'] as int;
/// String get title => json['title'] as String;
/// }
///
/// extension type BookInsert._(Map<String, dynamic> json) implements Object {
/// BookInsert({required String title}) : this._({'title': title});
/// }
///
/// extension type BookUpdate._(Map<String, dynamic> json) implements Object {
/// BookUpdate({String? title}) : this._({'title': ?title});
/// }
///
/// class Books {
/// static const table = PostgrestTable('books', Book.new);
/// static const table = PostgrestTable<Book, BookInsert, BookUpdate>(
/// 'books',
/// Book.new,
/// );
/// static const id = PostgrestColumn<Book, int>('id');
/// static const title = PostgrestColumn<Book, String>('title');
/// }
Expand All @@ -27,14 +39,25 @@ typedef RowConverter<Row> = Row Function(Map<String, dynamic> json);
/// .select()
/// .where(Books.title.like('%Dart%'))
/// .order(Books.id.desc());
///
/// await client.table(Books.table).insert(BookInsert(title: 'Dart'));
/// ```
///
/// [Insert] and [Update] are sent as the request body, so a value has to be
/// the JSON object to send, in practice an extension type over the
/// `Map<String, dynamic>` as above. Both have to be spelled out, since nothing
/// in the constructor arguments can infer them. A read-only relation, such as a
/// materialized view, uses `Never` for the write types it does not support,
/// which makes the corresponding methods uncallable.
///
/// Extension types over the decoded JSON map (as above) are the recommended
/// row representation since they carry no conversion cost and tolerate
/// partial selects, but any converter works, for example `Book.fromJson` on a
/// regular data class.
/// regular data class. `package:supabase_typegen` generates all three types
/// and the table definition from the database schema.
@experimental
class PostgrestTable<Row> {
// ignore: avoid-unused-generics
class PostgrestTable<Row, Insert, Update> {
const PostgrestTable(this.name, this.rowFromJson);

/// Name of the table in the database.
Expand Down
7 changes: 4 additions & 3 deletions packages/postgrest/lib/src/postgrest_typed_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ part 'postgrest_typed_query_builder.dart';
part 'postgrest_typed_transform_builder.dart';
part 'postgrest_typed_filter_builder.dart';

List<Row> _rowsFromJson<Row>(PostgrestTable<Row> table, PostgrestList rows) => [
for (final row in rows) table.rowFromJson(row),
];
List<Row> _rowsFromJson<Row>(
RowConverter<Row> rowFromJson,
PostgrestList rows,
) => [for (final row in rows) rowFromJson(row)];

/// The `select` parameter for [columns], or `*` when none are given.
String _selectList<Row>(List<PostgrestColumnExpression<Row, Object>>? columns) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class PostgrestTypedFilterBuilder<Row, T>
extends PostgrestTypedTransformBuilder<Row, T> {
const PostgrestTypedFilterBuilder._(
this._filterBuilder,
PostgrestTable<Row> table,
) : super._(_filterBuilder, table);
RowConverter<Row> rowFromJson,
) : super._(_filterBuilder, rowFromJson);

final PostgrestFilterBuilder<T> _filterBuilder;

Expand Down Expand Up @@ -42,6 +42,6 @@ class PostgrestTypedFilterBuilder<Row, T>
for (final parameter in filter.queryParameters) {
builder = builder.appendSearchParameter(parameter.key, parameter.value);
}
return PostgrestTypedFilterBuilder._(builder, _table);
return PostgrestTypedFilterBuilder._(builder, _rowFromJson);
}
}
124 changes: 100 additions & 24 deletions packages/postgrest/lib/src/postgrest_typed_query_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ part of 'postgrest_typed_builder.dart';
/// [PostgrestClient.table].
///
/// Query results are converted into [Row] through
/// [PostgrestTable.rowFromJson], so no raw `Map<String, dynamic>` is exposed.
/// [PostgrestTable.rowFromJson], so no raw `Map<String, dynamic>` is exposed,
/// and writes only accept the table's [Insert] and [Update] types.
/// {@endtemplate}
@experimental
class PostgrestTypedQueryBuilder<Row> {
class PostgrestTypedQueryBuilder<Row, Insert, Update> {
/// {@macro postgrest_typed_query_builder}
const PostgrestTypedQueryBuilder(
PostgrestQueryBuilder queryBuilder,
Expand All @@ -18,7 +19,7 @@ class PostgrestTypedQueryBuilder<Row> {
final PostgrestQueryBuilder _queryBuilder;

/// The table this builder queries.
final PostgrestTable<Row> table;
final PostgrestTable<Row, Insert, Update> table;

/// Perform a SELECT query on the table or view.
///
Expand All @@ -44,55 +45,124 @@ class PostgrestTypedQueryBuilder<Row> {
PostgrestFilterBuilder(
_queryBuilder
.select(_selectList(columns))
.withConverter((rows) => _rowsFromJson(table, rows)),
.withConverter((rows) => _rowsFromJson(table.rowFromJson, rows)),
),
table,
table.rowFromJson,
);

/// Perform an INSERT into the table or view.
/// Perform an INSERT of a single [row] into the table or view.
///
/// By default no data is returned. Use a trailing [select] to return the
/// inserted rows typed as [Row].
///
/// See [PostgrestQueryBuilder.insert] for [values] and [defaultToNull].
/// inserted row typed as [Row].
///
/// ```dart
/// final Book book = await client
/// .table(Books.table)
/// .insert({'title': 'foo'})
/// .insert(BookInsert(title: 'foo'))
/// .select()
/// .single();
/// ```
///
/// See [insertAll] to insert several rows in one request and
/// [PostgrestQueryBuilder.insert] for [defaultToNull].
PostgrestTypedFilterBuilder<Row, void> insert(
Object values, {
Insert row, {
bool defaultToNull = true,
}) => PostgrestTypedFilterBuilder._(
_queryBuilder.insert(values, defaultToNull: defaultToNull),
table,
_queryBuilder.insert(row as Object, defaultToNull: defaultToNull),
table.rowFromJson,
);

/// Perform an UPSERT on the table or view.
/// Perform an INSERT of every row in [rows] into the table or view.
///
/// ```dart
/// await client.table(Books.table).insertAll([
/// BookInsert(title: 'foo'),
/// BookInsert(title: 'bar'),
/// ]);
/// ```
///
/// [rows] needs at least one row.
///
/// See [insert] and [PostgrestQueryBuilder.insert] for [defaultToNull].
PostgrestTypedFilterBuilder<Row, void> insertAll(
List<Insert> rows, {
bool defaultToNull = true,
}) => PostgrestTypedFilterBuilder._(
_queryBuilder.insert(_nonEmpty(rows), defaultToNull: defaultToNull),
table.rowFromJson,
);

/// Perform an UPSERT of a single [row] on the table or view.
///
/// By default no data is returned. Use a trailing [select] to return the
/// upserted rows typed as [Row].
/// upserted row typed as [Row].
///
/// [onConflict] names the columns of the unique constraint to merge on;
/// left out, the primary key is the target.
///
/// ```dart
/// await client.table(Users.table).upsert(
/// {'email': 'a@example.com', 'name': 'Ada'},
/// UserInsert(email: 'a@example.com', name: 'Ada'),
/// onConflict: [Users.email],
/// );
/// ```
///
/// See [PostgrestQueryBuilder.upsert] for [values], [ignoreDuplicates] and
/// See [upsertAll] to upsert several rows in one request and
/// [PostgrestQueryBuilder.upsert] for [ignoreDuplicates] and
/// [defaultToNull].
PostgrestTypedFilterBuilder<Row, void> upsert(
Object values, {
Insert row, {
List<PostgrestColumn<Row, Object>>? onConflict,
bool ignoreDuplicates = false,
bool defaultToNull = true,
}) => _upsert(
row as Object,
onConflict: onConflict,
ignoreDuplicates: ignoreDuplicates,
defaultToNull: defaultToNull,
);

/// Perform an UPSERT of every row in [rows] on the table or view.
///
/// ```dart
/// await client.table(Users.table).upsertAll(
/// [
/// UserInsert(email: 'a@example.com', name: 'Ada'),
/// UserInsert(email: 'b@example.com', name: 'Bob'),
/// ],
/// onConflict: [Users.email],
/// );
/// ```
///
/// [rows] needs at least one row.
///
/// See [upsert] for [onConflict] and [PostgrestQueryBuilder.upsert] for
/// [ignoreDuplicates] and [defaultToNull].
PostgrestTypedFilterBuilder<Row, void> upsertAll(
List<Insert> rows, {
List<PostgrestColumn<Row, Object>>? onConflict,
bool ignoreDuplicates = false,
bool defaultToNull = true,
}) => _upsert(
_nonEmpty(rows),
onConflict: onConflict,
ignoreDuplicates: ignoreDuplicates,
defaultToNull: defaultToNull,
);

List<Insert> _nonEmpty(List<Insert> rows) {
if (rows.isEmpty) {
throw ArgumentError.value(rows, 'rows', 'rows needs at least one row');
}
return rows;
}

PostgrestTypedFilterBuilder<Row, void> _upsert(
Object values, {
required List<PostgrestColumn<Row, Object>>? onConflict,
required bool ignoreDuplicates,
required bool defaultToNull,
}) {
if (onConflict != null && onConflict.isEmpty) {
throw ArgumentError.value(
Expand All @@ -108,7 +178,7 @@ class PostgrestTypedQueryBuilder<Row> {
ignoreDuplicates: ignoreDuplicates,
defaultToNull: defaultToNull,
),
table,
table.rowFromJson,
);
}

Expand All @@ -120,11 +190,14 @@ class PostgrestTypedQueryBuilder<Row> {
/// ```dart
/// await client
/// .table(Books.table)
/// .update({'title': 'bar'})
/// .update(BookUpdate(title: 'bar'))
/// .where(Books.id.eq(1));
/// ```
PostgrestTypedFilterBuilder<Row, void> update(Map<String, dynamic> values) =>
PostgrestTypedFilterBuilder._(_queryBuilder.update(values), table);
PostgrestTypedFilterBuilder<Row, void> update(Update values) =>
PostgrestTypedFilterBuilder._(
_queryBuilder.update(values as Object),
table.rowFromJson,
);

/// Perform a DELETE on the table or view.
///
Expand All @@ -135,7 +208,7 @@ class PostgrestTypedQueryBuilder<Row> {
/// await client.table(Books.table).delete().where(Books.id.eq(1));
/// ```
PostgrestTypedFilterBuilder<Row, void> delete() =>
PostgrestTypedFilterBuilder._(_queryBuilder.delete(), table);
PostgrestTypedFilterBuilder._(_queryBuilder.delete(), table.rowFromJson);

/// Only performs a count query on the table or view.
///
Expand All @@ -144,5 +217,8 @@ class PostgrestTypedQueryBuilder<Row> {
/// ```
PostgrestTypedFilterBuilder<Row, int> count([
CountOption option = CountOption.exact,
]) => PostgrestTypedFilterBuilder._(_queryBuilder.count(option), table);
]) => PostgrestTypedFilterBuilder._(
_queryBuilder.count(option),
table.rowFromJson,
);
}
Loading