You ain't got time. I ain't got time neither. Let's get over with this pretty quick. Here are some of the facets of the Aqueduct framework you stand to understand in this guide.
- Starting an Aqueduct project
- Handling Migrations and providing seed data or fixtures for migrations
- Routing i.e handling params and query params
- Making CRUD requests, handled by a controller
- Using PostgreSQL as backend
- Basic pagination in Aqueduct
- Writing basic tests.
The reason behind the article (as well as many of my others) is the frustration I went through getting the most basic things done, and the unnecessary lengthy process other tutorials/articles go through, winding long and time wasting. For the full rant, see at the end.
TL;DR
Without much ado, let's rock and roll
Setting Up Project
I'm not gonna pamper you. Here's how to setup an Aqueduct project. You don't have time, so just straight to the meat
- Install Dart
- Install Postgres Database
- Install Aqueduct
- Clone the Aqueduct-CRUD Repository
Setting up Database
Create a postgreSQL database, call it anything you want.
$ sudo -u postgres psql
> CREATE USER projectuser WITH PASSWORD 'password';
> CREATE DATABASE mydatabase;
> GRANT ALL PRIVILEGES ON DATABASE mydatabase TO projectuser;
> \q
Then create a database.yaml
file in the root project (same location as the pubspec.yaml
), and put in these:
username: 'projectuser'
password: 'password'
host: 'localhost'
port: 5432
databaseName: 'mydatabase'
The above database.yaml
is for the aqueduct db
commands to make connections to the DB. However, for our application to speak to the database, we need update the config.yaml
with our database info.
database:
host: 'localhost'
port: 5432
username: 'projectuser'
password: 'password'
databaseName: 'mydatabase'
Migration
If you're coming from somewhere like Django or Laravel, migration is likely a piece of cake for you.
Otherwise, think of migrations as a way to communicate your schema changes to the database, for real. If you didn't get that, please google for more details on migrations
TL;DR
Last but not least, run the migration, to convey what database scheme we have and want to the database for real. You can check the migrations/00000001_initial.migration.dart
file to see what is gonna happen.
Also, seed/fixture data is added for free. Duh!
Run aqueduct db upgrade
to commit the migration to database.
Run Application
In the project root folder, run aqueduct serve
Since this project is API endpoints only, you'll have to consume using a REST Client, such as Postman.
If using Postman, check out this API request collection to help you get started with consuming your endpoints. You might wanna read on how to use the collection in Postman.
We've spent 2 seconds already. That's too much time. Let's actually get into the real deals
Application Channel, Entry Point & Routing
In the lib/channel.dart
is where we find, more or less, the bootstrapping of our application, and the few get-outta-ways we'll want done, such as, keeping in touch with the database by loading the config file, and also create one or two routes.
Nothing extraordinary happening. Just the same principles you know from your Ruby, DJango or Laravel. Heck, even Angular!
Update your lib/channel.dart
file with these contents
import 'controller/icd_controller.dart';
import 'dart:io';
import 'mpharma.dart';
class MpharmaChannel extends ApplicationChannel {
ManagedContext context;
@override
Future prepare() async {
logger.onRecord.listen(
(rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}"));
// this Class is at the bottom
final config = DatabaseConfig(options.configurationFilePath);
final dataModel = ManagedDataModel.fromCurrentMirrorSystem();
final persistentStore = PostgreSQLPersistentStore.fromConnectionInfo(
config.database.username,
config.database.password,
config.database.host,
config.database.port,
config.database.databaseName,
);
context = ManagedContext(dataModel, persistentStore);
}
@override
Controller get entryPoint {
final router = Router();
router.route("/example").linkFunction((request) async {
return Response.ok({"key": "value"});
});
router.route('/icd/[:id]').link(() => ICDController(context));
return router;
}
}
class DatabaseConfig extends Configuration {
DatabaseConfig(String path) : super.fromFile(File(path));
DatabaseConfiguration database;
@optionalConfiguration
int identifier;
}
In English:
- We're importing a few.
dart:io
allows us to read files (we're reading theconfig.yaml
file. Think of it as magic! - We also load a controller file. If coming from Angular, think of it as the Component.ts file
- Then we create some routes. This part, lemme explain a bit
router.route('/icd/[:id]').link(() => ICDController(context));
A lot is packed into the above single line of code. So much, I can write a book on this line alone. But well...
The above route matches anything like this
example.com/icd GET
example.com/icd POST
example.com/icd/1 GET
example.com/icd/1 PUT
example.com/icd/1 DELETE
example.com/icd?limit=x&offset=x
The [:id]
is the params. The [ ]
around it makes that param optional. Thus, without an ID param, it'll do just fine. So if example.com/icd
comes in, the application will do just fine.
You'll see how each of these HTTP verbs are handled in the controller when we switch over to it next. However, know that we're using the HTTP verbs, as many as possible, at least to distinguish the requests
The ICDController()
was imported from the controller, which handles the routes coming through this endpoint.
To pass query params, just pass it in the URL, like it's done in other frameworks. You only deal with the query params, if any, when in the controller.
So you see, just a single line, all the punch it throws? Excited yet? If not, the Controller might whet you up!
Mr. Controller
Our Component Controller is where we handle the logic and communicates through the ORM that comes with Aqueduct to the Database. Above, we setup to use PostgreSQL, which depending on who you ask, is the best Relational Database platform in the world.
'Abeg', let's end the DB wars here!
Like other platforms, having a model for our database means we can more or less regulate what data structures flows through our application, and into the database.
In Aqueduct, we do so, by creating a model/icd_model.dart
import 'package:mpharma/mpharma.dart';
class ICD extends ManagedObject<_ICD> implements _ICD {
@override
void willInsert() {
createdAt = DateTime.now().toUtc();
}
}
class _ICD {
@primaryKey
int id;
@Column()
String categoryCode;
@Column()
String diagnosisCode;
@Column()
String fullCode;
@Column()
String abbrDesc;
@Column()
String fullDesc;
@Column()
String categoryTitle;
@Column()
DateTime createdAt;
}
The only part I'll comment on is
@override
void willInsert() {
createdAt = DateTime.now().toUtc();
}
In Mongoose (ExpressJS, NodeJS, MongoDB), there's the { timestamp: true }
option, which when specified, any data passing through the schema into the database, gets a createdAt
and updatedAt
timestamps. It happens for free, seamlessly
In Aqueduct, a similar result can be achieved via the model signals. In the case above, whenever there's an insert about to happen - willInsert()
, cook up a createdAt
field, slap in there the current Datetime in UTC format, then add as a field to whatever data structure is about to be saved.
The rest of the code is, well, code!
I keep dangling the controller in your face, but not showing you the code? Here we go
import 'package:aqueduct/aqueduct.dart';
import 'package:mpharma/mpharma.dart';
import 'package:mpharma/model/icd_model.dart';
class ICDController extends ResourceController {
ICDController(this.context);
final ManagedContext context;
@Operation.get()
Future<Response> getAll({@Bind.query('limit') int limit, @Bind.query('offset') int pageby}) async {
limit ??= 100;
pageby ??= 0;
if (pageby != null) { pageby = pageby * 10; }
final icdQuery = Query<ICD>(context);
icdQuery
..pageBy((p) => p.createdAt, QuerySortOrder.descending)
..fetchLimit = limit
..offset = pageby;
final icds = await icdQuery.fetch();
return Response.ok(icds);
}
@Operation.get('id')
Future<Response> getById(@Bind.path('id') int id) async {
final query = Query<ICD>(context)..where((icd) => icd.id).equalTo(id);
final icd = await query.fetchOne();
if (icd == null) {
return Response.notFound(body: 'Not found');
}
return Response.ok(icd);
}
@Operation.post()
Future<Response> createICD(@Bind.body() ICD body) async {
final query = Query<ICD>(context)..values = body;
final insertICD = await query.insert();
return Response.ok(insertICD);
}
@Operation.put('id')
Future<Response> updateById(
@Bind.path('id') int id, @Bind.body() ICD body) async {
final query = Query<ICD>(context)
..where((icd) => icd.id).equalTo(id)
..values = body;
final icd = await query.updateOne();
if (icd == null) {
return Response.notFound(body: 'Not found');
}
return Response.ok(icd);
}
@Operation.delete('id')
Future<Response> deleteById(@Bind.path('id') int id) async {
final query = Query<ICD>(context)..where((icd) => icd.id).equalTo(id);
final icd = await query.delete();
return Response.ok({'state': true, 'msg': 'Delete successfull'});
}
}
A lot to unpack here? Nope. Just a few
@Operation.get()
Future<Response> getAll({@Bind.query('limit') int limit, @Bind.query('offset') int pageby}) async {
print(limit);
limit ??= 100;
pageby ??= 0;
@Operation.get()
, @Operation.post()
etc, is more like the router.get('/url', req, res)
in Express
Future<Response> getAll({@Bind.query('limit') int limit, @Bind.query('offset') int pageby}) async { .. }
Take note of this part first,getAll({@Bind.query('limit') int limit, @Bind.query('offset') int pageby})
When you do getAll(@Bind.query...)
it means totally different from getAll({@Bind.query ...})
Do you notice the difference? The second one is in curly braces. The first is not.
Within curly braces mean it's optional parameters, thus, in the above within the braces, if the URL doesn't supply any query params, we'll do just fine.
limit ??= 100;
pageby ??= 0;
// should this next line even be there? I wrote it
// Can't tell for sure if it should be there. ¯\_(ツ)_/¯
if (pageby != null) { pageby = pageby * 10; }
final icdQuery = Query<ICD>(context);
icdQuery
..pageBy((p) => p.createdAt, QuerySortOrder.descending)
..fetchLimit = limit
..offset = pageby;
limit ??= 100;
is basically saying if the limit
query param is empty, just assign 100 to that var. Same for the pageby
The rest, is just straight code written in English. If any part of the controller code isn't clear, kindly leave a comment. Otherwise I take it as y'all readers (more like my classroom students) said "Yes sir, we understand" to me.
Conclusion
And so we're done. Source code to above project here:
Leave any comments you might have in the comment section below.
If you're into tests, check the sample Test I wrote for this project.
Top comments (0)