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
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,57 @@
## 1.14.0

- New `DBSQLiteAdapter`: an embedded SQLite DB adapter, backed by the
[`sqlite3`][sqlite3_pkg] package.

```dart
import 'package:bones_api/bones_api_db_sqlite.dart';

var adapter = DBSQLiteAdapter('/var/lib/myapp/db.sqlite',
generateTables: true);

// Or an in-memory database:
var memoryAdapter = DBSQLiteAdapter(':memory:', generateTables: true);
```

- Registered as `sqlite`, `sqlite3`, `sql.sqlite` and `sql.sqlite3`, so a
config block `db: { sqlite: {...} }` resolves it.
- `fromConfig` accepts `path`/`file`/`database`/`db` for the database file,
and `memory: true` (or the path `:memory:`) for an in-memory database, plus
the usual `generateTables`/`checkTables`/`populate`/`log.sql` keys.
Irrelevant keys (`host`, `port`, `username`, `password`) are accepted and
ignored, so a config can be pointed at SQLite without being rewritten.
- **No server and no native library to install**: the `sqlite3` package
bundles SQLite (3.53.4) through Dart's build hooks.
- Runs the same entity test-suite as the PostgreSQL and MySQL adapters, and
needs no `Docker` container to do it. New `APITestConfigSQLite`, exported by
`package:bones_api/bones_api_test_sqlite.dart`.

- Notes on the SQLite dialect:
- An auto-assigning ID is declared `INTEGER PRIMARY KEY AUTOINCREMENT`:
SQLite has no `SERIAL`/`AUTO_INCREMENT`, only a column declared exactly
`INTEGER PRIMARY KEY` aliases the `rowid`, and without `AUTOINCREMENT`
SQLite reuses the ID of a deleted row.
- `ENUM` is emulated with a `VARCHAR CHECK (col IN (...))` constraint.
- Since `sqlite3` is a **synchronous** driver, and SQLite allows a single
writer, the adapter uses one native handle shared by every pooled
connection: a second handle blocking on a lock would stall the isolate
holding it, and offers nothing to gain when there is no I/O to overlap.
Nested transactions use `SAVEPOINT`.

- New `SQLDialect.returningAcceptsTableWildcard` (default `true`, so the
PostgreSQL/MySQL/memory dialects are unchanged). SQLite rejects the
table-qualified wildcard that `DELETE ... RETURNING` emits
(*"RETURNING may not use TABLE.\* wildcards"*) and needs a bare
`RETURNING *`.

- **Breaking**: the minimum Dart SDK is now **3.10.0** (was 3.7.0), required by
`sqlite3` and its build hooks.

- Dependencies:
- Added `sqlite3: ^3.5.1`

[sqlite3_pkg]: https://pub.dev/packages/sqlite3

## 1.13.0

- New `EntityPagination.onEvent`: an optional hook notified of what is being
Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,15 +539,34 @@ postgres:

To use a SQL database with your `EntityRepository` you need a `SQLAdapter`:

- `PostgreSQLAdapter`: a [PostgreSQL][postgres] adapter.
- `MySQLAdapter`: A [MySQL][mysql] adapter.
- `MemorySQLAdapter`: a portable `SQLAdapter` that stores entities in memory.
- `DBPostgreSQLAdapter`: a [PostgreSQL][postgres] adapter.
Import: `package:bones_api/bones_api_db_postgre.dart`
- `DBMySQLAdapter`: a [MySQL][mysql] adapter.
Import: `package:bones_api/bones_api_db_mysql.dart`
- `DBSQLiteAdapter`: an embedded [SQLite][sqlite] adapter, for a database file
or an in-memory database. Needs no server and no native library: the
[`sqlite3`][sqlite3_pkg] package bundles SQLite itself.
Import: `package:bones_api/bones_api_db_sqlite.dart`
- `DBSQLMemoryAdapter`: a portable `SQLAdapter` that stores entities in memory.

The `SQLAdapter` is responsible to connect to the database, manage the connection
pool and also to adjust the generated SQLs to the correct dialect.

Example of a SQLite configuration:

```yaml
db:
sqlite:
path: /var/lib/myapp/db.sqlite
generateTables: true
```

Use `memory: true` (or `path: ':memory:'`) for an in-memory database.

[postgres]: https://www.postgresql.org/
[mysql]: https://www.mysql.com/
[sqlite]: https://www.sqlite.org/
[sqlite3_pkg]: https://pub.dev/packages/sqlite3

## Bones_UI

Expand Down
41 changes: 20 additions & 21 deletions bin/bones_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@ void main(List<String> args) async {
await commandInfo.configure();
await commandCreate.configure();

var commandRunner =
CommandRunner<bool>('bones_api', '$cliTitle - CLI Tool')
..addCommand(MyCommandServe())
..addCommand(MyCommandConsole())
..addCommand(MyCommandInspect())
..addCommand(commandInfo)
..addCommand(commandCreate);
var commandRunner = CommandRunner<bool>('bones_api', '$cliTitle - CLI Tool')
..addCommand(MyCommandServe())
..addCommand(MyCommandConsole())
..addCommand(MyCommandInspect())
..addCommand(commandInfo)
..addCommand(commandCreate);

commandRunner.argParser.addFlag(
'version',
Expand Down Expand Up @@ -248,19 +247,17 @@ class MyCommandServe extends CommandSourceFileBase {
var val = argResults!['domain'];
if (val == null) return <String, String>{};

var values =
(val is List ? val : [val])
.map((e) => e != null ? '$e'.trim() : '')
.where((e) => e.isNotEmpty)
.toList();
var values = (val is List ? val : [val])
.map((e) => e != null ? '$e'.trim() : '')
.where((e) => e.isNotEmpty)
.toList();

var entries =
values.map((e) {
var parts = e.split('=');
var domain = parts[0].trim();
var path = parts.length > 1 ? parts[1].trim() : '';
return MapEntry(domain, path);
}).toList();
var entries = values.map((e) {
var parts = e.split('=');
var domain = parts[0].trim();
var path = parts.length > 1 ? parts[1].trim() : '';
return MapEntry(domain, path);
}).toList();

return Map<String, String>.fromEntries(entries);
}
Expand Down Expand Up @@ -635,7 +632,8 @@ class MyCommandServe extends CommandSourceFileBase {
String projectLibraryName,
String apiRootClass,
) {
var script = '''
var script =
'''
import 'package:bones_api/bones_api_server.dart';
import 'package:bones_api/bones_api_dart_spawner.dart';

Expand Down Expand Up @@ -852,7 +850,8 @@ class MyCommandConsole extends CommandSourceFileBase {
String projectLibraryName,
String apiRootClass,
) {
var script = '''
var script =
'''
import 'dart:async';
import 'dart:convert';
import 'dart:io';
Expand Down
3 changes: 3 additions & 0 deletions dart_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ tags:
# Tests that uses `MySQL`.
mysql:
timeout: 180s
# Tests that uses `SQLite` (embedded: no container needed).
sqlite:
timeout: 180s
# Slow tests.
slow:
timeout: 180s
Expand Down
3 changes: 2 additions & 1 deletion example/bones_api_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class MyBTCModule extends APIModule {
// The requested path:
var path = request.path;

var body = '''
var body =
'''
<h1>404</h1><br>
<b>PATH:<b> $path
<p>
Expand Down
4 changes: 4 additions & 0 deletions lib/bones_api_db_sqlite.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// Bones_API DB Adapter for SQLite.
library;

export 'src/bones_api_entity_db_sqlite.dart';
5 changes: 5 additions & 0 deletions lib/bones_api_test_sqlite.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// Bones_API Test SQLite Library.
library;

export 'bones_api_test.dart';
export 'src/bones_api_test_utils_sqlite.dart';
18 changes: 8 additions & 10 deletions lib/src/bones_api_authentication.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ class APICredential {
this.refreshToken,
String? passwordHash,
APIPasswordHashAlgorithm? hashAlgorithm,
}) : password =
passwordHash != null
? APIPassword(passwordHash, hashAlgorithm: hashAlgorithm)
: null;
}) : password = passwordHash != null
? APIPassword(passwordHash, hashAlgorithm: hashAlgorithm)
: null;

APICredential._(
this.username,
Expand Down Expand Up @@ -201,8 +200,8 @@ abstract class APIPasswordHashAlgorithm {
/// Ensures that [passwordOrHash] is hashed with this algorithm.
String ensureHashedPassword(String passwordOrHash) =>
isHashedPassword(passwordOrHash)
? passwordOrHash
: hashPassword(passwordOrHash);
? passwordOrHash
: hashPassword(passwordOrHash);

@override
String toString() {
Expand Down Expand Up @@ -480,10 +479,9 @@ class APIToken implements Comparable<APIToken> {
}) : token = token ?? generateToken(512, variableLength: 32, prefix: 'TK'),
issueTime = issueTime ?? DateTime.now(),
duration = duration ?? Duration(hours: 3),
refreshToken =
refreshToken == null && withRefreshToken
? generateToken(640, variableLength: 64, prefix: 'RTK')
: refreshToken;
refreshToken = refreshToken == null && withRefreshToken
? generateToken(640, variableLength: 64, prefix: 'RTK')
: refreshToken;

DateTime get accessTime => _accessTime;

Expand Down
Loading