Skip to content

Commit

Permalink
Add multi-package example
Browse files Browse the repository at this point in the history
  • Loading branch information
simolus3 committed Jan 30, 2024
1 parent b58e97f commit af55bd0
Show file tree
Hide file tree
Showing 29 changed files with 1,130 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/pages/docs/Examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ drift features:
- The [migration] example makes use of advanced schema migrations and shows how
to test migrations between different database schemas by using drift's
[dedicated tooling][migration tooling] for this purpose.
- There's an example showing how to share drift database definitions between a
[server and a client][multi_package] in different packages.
- [Another example][with_built_value] shows how to use drift-generated code in
other builders (here, `built_value`).

Expand Down Expand Up @@ -53,3 +55,5 @@ Additional patterns are also shown and explained on this website:
[migration]: https://github.com/simolus3/drift/tree/develop/examples/migrations_example
[migration tooling]: {{ '../Migrations/tests.md#verifying-migrations' | pageUrl }}
[with_built_value]: https://github.com/simolus3/drift/tree/develop/examples/with_built_value
[multi_package]: https://github.com/simolus3/drift/tree/develop/examples/multi_package

1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This collection of examples demonstrates how to use some advanced drift features
- `migrations_example`: Example showing to how to generate test utilities to verify schema migration.
- `modular`: Example using drift's upcoming modular generation mode.
- `with_built_value`: Configure `build_runner` so that drift-generated classes can be used by `build_runner`.
- `multi_package`: This example shows how to share drift definitions between packages.

These two examples exist to highlight a feature of `package:drift/web.dart` and `package:drift/web/workers.dart`.
However, the setup shown here is now a core drift feature thanks to `WasmDatabase.open`, which means that this
Expand Down
32 changes: 32 additions & 0 deletions examples/multi_package/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
This example shows how to use drift declarations across packages.
It is structured as follows:

- `shared/` contains table definitions. This package does not define a database
on its own (although that could be useful for testing), instead it declares
tables used by the server and the client.
- `server/` is a simple shelf server using Postgres with drift.
- `client/` is a simple CLI client using a local sqlite3 database
while also communicating with the server.

As the main point of this example is to demonstrate how the build
setup could look like, the client and server are kept minimal.

To fully build the code, `build_runner run` needs to be run in all three
packages.
However, after making changes to the database code in one of the packages, only
that package needs to be rebuilt.

## Starting

To run the server, first start a postgres database server:

```
docker run -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres postgres
```

Then, run the example by starting a server and a client:

```
dart run server/bin/server.dart
dart run client/bin/client.dart
```
3 changes: 3 additions & 0 deletions examples/multi_package/client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
30 changes: 30 additions & 0 deletions examples/multi_package/client/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.

include: package:lints/recommended.yaml

# Uncomment the following section to specify additional rules.

# linter:
# rules:
# - camel_case_types

# analyzer:
# exclude:
# - path/to/excluded/files/**

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints

# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
33 changes: 33 additions & 0 deletions examples/multi_package/client/bin/client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'dart:convert';

import 'package:client/database.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:http/http.dart' as http;
import 'package:shared/tables.dart';

void main(List<String> arguments) async {
final database = ClientDatabase(NativeDatabase.memory());
final client = http.Client();

// Fetch posts from server and save them in the local database.
final fromServer =
await client.get(Uri.parse('http://localhost:8080/posts/latest'));

await database.batch((batch) {
final entries = json.decode(fromServer.body) as List;

for (final entry in entries) {
final post = Post.fromJson(entry['post']);
final user = User.fromJson(entry['author']);

batch.insert(database.posts, post);
batch.insert(database.users, user, onConflict: DoUpdate((old) => user));
}
});

final localPosts = await database.locallySavedPosts;
print('Saved local posts: $localPosts');

await database.close();
}
25 changes: 25 additions & 0 deletions examples/multi_package/client/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
targets:
$default:
builders:
drift_dev:
# disable drift's default builder, we're using the modular setup
# instead.
enabled: false

# Instead, enable drift_dev:analyzer and drift_dev:modular manually:
drift_dev:analyzer:
enabled: true
options: &options
# Drift build options, as per https://drift.simonbinder.eu/docs/advanced-features/builder_options/
store_date_time_values_as_text: true
named_parameters: true
sql:
dialects:
- sqlite
options:
version: "3.45"
modules: [fts5]
drift_dev:modular:
enabled: true
# We use yaml anchors to give the two builders the same options
options: *options
17 changes: 17 additions & 0 deletions examples/multi_package/client/lib/database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:drift/drift.dart';

import 'database.drift.dart';

@DriftDatabase(include: {'package:shared/shared.drift'})
class ClientDatabase extends $ClientDatabase {
ClientDatabase(super.e);

@override
int get schemaVersion => 1;

Future<int> get locallySavedPosts async {
final count = countAll();
final query = selectOnly(posts)..addColumns([count]);
return query.map((row) => row.read(count)!).getSingle();
}
}
22 changes: 22 additions & 0 deletions examples/multi_package/client/lib/database.drift.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:shared/src/users.drift.dart' as i1;
import 'package:shared/src/posts.drift.dart' as i2;
import 'package:shared/shared.drift.dart' as i3;
import 'package:drift/internal/modular.dart' as i4;

abstract class $ClientDatabase extends i0.GeneratedDatabase {
$ClientDatabase(i0.QueryExecutor e) : super(e);
late final i1.$UsersTable users = i1.$UsersTable(this);
late final i2.Posts posts = i2.Posts(this);
i3.SharedDrift get sharedDrift => i4.ReadDatabaseContainer(this)
.accessor<i3.SharedDrift>(i3.SharedDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@override
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [users, posts];
@override
i0.DriftDatabaseOptions get options =>
const i0.DriftDatabaseOptions(storeDateTimeAsText: true);
}
17 changes: 17 additions & 0 deletions examples/multi_package/client/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: client
publish_to: none

environment:
sdk: ^3.2.5

dependencies:
drift: ^2.15.0
http: ^1.2.0
shared:
path: ../shared/

dev_dependencies:
lints: ^2.1.0
test: ^1.24.0
drift_dev: ^2.15.0
build_runner: ^2.4.8
3 changes: 3 additions & 0 deletions examples/multi_package/server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
30 changes: 30 additions & 0 deletions examples/multi_package/server/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.

include: package:lints/recommended.yaml

# Uncomment the following section to specify additional rules.

# linter:
# rules:
# - camel_case_types

# analyzer:
# exclude:
# - path/to/excluded/files/**

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints

# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
75 changes: 75 additions & 0 deletions examples/multi_package/server/bin/server.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'dart:convert';
import 'dart:io';

import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:postgres/postgres.dart';
import 'package:server/database.dart';
import 'package:shared/tables.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';

// To run this server, first start a local postgres server with
//
// docker run -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres postgres
//
void main(List<String> args) async {
// Use any available host or container IP (usually `0.0.0.0`).
final ip = InternetAddress.anyIPv4;
final database = ServerDatabase(PgDatabase(
endpoint: Endpoint(
host: 'localhost',
database: 'postgres',
username: 'postgres',
password: 'postgres',
),
settings: ConnectionSettings(
// Disable because this example is talking to a local postgres container.
sslMode: SslMode.disable,
),
));

final router = Router()
..post('/post', (Request request) async {
final header = request.headers['Authorization'];
if (header == null || !header.startsWith('Bearer ')) {
return Response.unauthorized('Missing Authorization header');
}

final user =
await database.authenticateUser(header.substring('Bearer '.length));
if (user == null) {
return Response.unauthorized('Invalid token');
}

database.posts.insertOne(PostsCompanion.insert(
author: user.id, content: Value(await request.readAsString())));

return Response(201);
})
..get('/posts/latest', (req) async {
final somePosts = await database.sharedDrift
.allPosts(limit: (_, __) => Limit(10, null))
.get();

return Response.ok(
json.encode([
for (final post in somePosts)
{
'author': post.author,
'post': post.posts,
}
]),
headers: {'Content-Type': 'application/json'},
);
});

// Configure a pipeline that logs requests.
final handler = Pipeline().addMiddleware(logRequests()).addHandler(router);

// For running in containers, we respect the PORT environment variable.
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final server = await serve(handler, ip, port);
print('Server listening on port ${server.port}');
}
26 changes: 26 additions & 0 deletions examples/multi_package/server/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
targets:
$default:
builders:
drift_dev:
# disable drift's default builder, we're using the modular setup
# instead.
enabled: false

# Instead, enable drift_dev:analyzer and drift_dev:modular manually:
drift_dev:analyzer:
enabled: true
options: &options
# Drift build options, as per https://drift.simonbinder.eu/docs/advanced-features/builder_options/
store_date_time_values_as_text: true
named_parameters: true
sql:
# The server itself will only support postgres
dialects:
- postgres
options:
version: "3.45"
modules: [fts5]
drift_dev:modular:
enabled: true
# We use yaml anchors to give the two builders the same options
options: *options
43 changes: 43 additions & 0 deletions examples/multi_package/server/lib/database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:drift/drift.dart';
import 'package:shared/tables.dart';

import 'database.drift.dart';

// Additional table we only need on the server
class ActiveSessions extends Table {
IntColumn get user => integer().references(Users, #id)();
TextColumn get bearerToken => text()();
}

@DriftDatabase(
tables: [ActiveSessions],
include: {'package:shared/shared.drift'},
)
class ServerDatabase extends $ServerDatabase {
ServerDatabase(super.e);

@override
int get schemaVersion => 1;

@override
MigrationStrategy get migration {
return MigrationStrategy(
beforeOpen: (details) async {
if (details.wasCreated) {
await users.insertOne(UsersCompanion.insert(name: 'Demo user'));
await posts.insertOne(
PostsCompanion.insert(author: 1, content: Value('Test post')));
}
},
);
}

Future<User?> authenticateUser(String token) async {
final query = select(users).join(
[innerJoin(activeSessions, activeSessions.user.equalsExp(users.id))]);
query.where(activeSessions.bearerToken.equals(token));

final row = await query.getSingleOrNull();
return row?.readTable(users);
}
}
Loading

0 comments on commit af55bd0

Please sign in to comment.