-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 23ed473
Showing
8 changed files
with
281 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.dart_tool/ | ||
pubspec.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"version": "0.2.0", | ||
"configurations": [ | ||
{ | ||
"name": "Launch example", | ||
"request": "launch", | ||
"type": "dart", | ||
"program": "example/unpub_sqlite_example.dart" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# unpub_sqlite | ||
|
||
A package implementing a unpub's `MetaStore` for storing package metadata in a local SQLite database. | ||
|
||
## Motivation | ||
|
||
[Unpub](https://github.com/bytedance/unpub) is a decent solution for self-hosting a Dart package repository. However, it has a forced dependency on MongoDB, which can be an obstacle for those who prefer not to run additional services. Since Unpub already supports storing packages on the filesystem, replacing MongoDB with SQLite was a no-brainer to make Unpub more lightweight and without dependencies on other services (which are much more heavy than the unpub itself). | ||
|
||
## Features and Missing Features | ||
|
||
- ✅ Automatically initializes a new SQLite database | ||
- ❌ Migrations between versions of SQLite databases | ||
- ✅ Stores packages and their versions | ||
- ✅ Stores uploaders | ||
- ✅ Querying packages by keyword | ||
- ❌ Querying packages by uploader | ||
- ❌ Querying packages by dependency | ||
- ❌ Any test coverage | ||
|
||
## Disclaimer | ||
|
||
The Unpub project itself is currently completely abandoned (last update 2021), and this package is likely to share the same fate. This package was created to fulfill personal requirements, and some features may be incomplete or missing. | ||
|
||
You're welcome to open issues, but please don't expect them to be resolved promptly or at all. Contributions via pull requests are highly encouraged, it’s just a single-file package after all! | ||
|
||
## Resources | ||
|
||
- [Unpub Repository](https://github.com/bytedance/unpub) | ||
- [SQLite](https://sqlite.org/index.html) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import 'package:sqlite3/sqlite3.dart'; | ||
import 'package:unpub/unpub.dart' as unpub; | ||
import 'package:unpub_sqlite/src/sqlite_meta_store.dart'; | ||
|
||
void main(List<String> args) async { | ||
print('Using sqlite3 ${sqlite3.version}'); | ||
|
||
// To use a database backed by a file, you | ||
// can replace this with sqlite3.open(yourFilePath). | ||
final db = sqlite3.openInMemory(); | ||
|
||
final metaStore = SqliteMetaStore(sqliteDatabase: db); | ||
metaStore.initialize(); | ||
|
||
final app = unpub.App( | ||
metaStore: metaStore, | ||
packageStore: unpub.FileStore('./unpub-packages'), | ||
); | ||
|
||
final server = await app.serve('0.0.0.0', 4000); | ||
print('Serving server at http://${server.address.host}:${server.port}'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
class UnpubSqliteException implements Exception {} | ||
|
||
class AutocommitRequired extends UnpubSqliteException { | ||
@override | ||
String toString() => | ||
"Auto-commit is required to be enabled on the Database instance"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
import 'dart:convert'; | ||
|
||
import 'package:sqlite3/sqlite3.dart'; | ||
import 'package:unpub/unpub.dart'; | ||
|
||
import 'exceptions.dart'; | ||
|
||
const uploadersTableName = 'uploaders'; | ||
const packagesTableName = 'packages'; | ||
const versionsTableName = 'versions'; | ||
|
||
class SqliteMetaStore extends MetaStore { | ||
final Database sqliteDatabase; | ||
|
||
SqliteMetaStore({required this.sqliteDatabase}) { | ||
if (!sqliteDatabase.autocommit) throw AutocommitRequired(); | ||
} | ||
|
||
void initialize() { | ||
sqliteDatabase.execute(''' | ||
create table if not exists $packagesTableName ( | ||
id integer not null primary key, | ||
name text not null unique, | ||
downloads integer not null default 0, | ||
created_at datetime default current_timestamp, | ||
updated_at datetime default current_timestamp | ||
); | ||
create table if not exists $uploadersTableName ( | ||
id integer not null primary key, | ||
package_id integer not null, | ||
email text not null, | ||
foreign key (package_id) references $packagesTableName(id) | ||
); | ||
create table if not exists $versionsTableName ( | ||
id integer not null primary key, | ||
package_id integer not null, | ||
data text not null, | ||
foreign key (package_id) references $packagesTableName(id) | ||
); | ||
'''); | ||
} | ||
|
||
@override | ||
Future<void> addUploader(String name, String email) async { | ||
final packageId = getPackageId(name); | ||
if (packageId == null) return; // FIXME: silent error? | ||
|
||
sqliteDatabase.prepare(''' | ||
insert | ||
into $uploadersTableName | ||
(package_id, email) values (?, ?) | ||
''').execute([packageId, email]); | ||
} | ||
|
||
@override | ||
Future<void> removeUploader(String name, String email) async { | ||
final packageId = getPackageId(name); | ||
if (packageId == null) return; // FIXME: silent error? | ||
|
||
sqliteDatabase.prepare(''' | ||
delete | ||
from $uploadersTableName | ||
where | ||
package_id = ? AND email = ? | ||
''').execute([packageId, email]); | ||
} | ||
|
||
@override | ||
void increaseDownloads(String name, String version) { | ||
sqliteDatabase.prepare(''' | ||
update | ||
$packagesTableName | ||
set | ||
downloads = downloads + 1 | ||
where | ||
name = ? | ||
''').execute([name]); | ||
} | ||
|
||
@override | ||
Future<void> addVersion(String name, UnpubVersion version) async { | ||
final versionJsonData = json.encode( | ||
version.toJson(), | ||
toEncodable: (obj) => obj is DateTime ? obj.toIso8601String() : obj, | ||
); | ||
|
||
// Upsert to packages | ||
sqliteDatabase.prepare(''' | ||
insert | ||
into $packagesTableName | ||
(name) values (?) | ||
on conflict (name) | ||
do update set updated_at = current_timestamp | ||
''').execute([name]); | ||
|
||
final packageId = sqliteDatabase.lastInsertRowId; | ||
|
||
// Create new version | ||
sqliteDatabase.prepare(''' | ||
insert | ||
into $versionsTableName | ||
(data, package_id) values (?, ?) | ||
''').execute([versionJsonData, packageId]); | ||
} | ||
|
||
@override | ||
Future<UnpubPackage?> queryPackage(String name) async { | ||
final packageId = getPackageId(name); | ||
if (packageId == null) return null; | ||
return queryPackageById(packageId); | ||
} | ||
|
||
@override | ||
Future<UnpubQueryResult> queryPackages( | ||
{required int size, | ||
required int page, | ||
required String sort, | ||
String? keyword, | ||
String? uploader, | ||
String? dependency}) async { | ||
// FIXME: Querying by dependency is not supported, returns none | ||
if (dependency != null) return UnpubQueryResult(0, []); | ||
|
||
final likeKeyword = keyword != null ? '%$keyword%' : '%'; | ||
final likeUploader = uploader != null ? '%$uploader%' : '%'; | ||
|
||
final filterQueryPartial = ''' | ||
from $packagesTableName p | ||
full join $uploadersTableName u on p.id = u.package_id | ||
where | ||
p.name like ? | ||
and coalesce(u.email,'') like ? | ||
'''; | ||
|
||
final countPackagesQuery = sqliteDatabase.prepare(''' | ||
select count(*) | ||
$filterQueryPartial | ||
''').select([likeKeyword, likeUploader]); | ||
|
||
final foundPackagesQuery = sqliteDatabase.prepare(''' | ||
select distinct p.id | ||
$filterQueryPartial | ||
order by ? | ||
limit ? | ||
offset ? | ||
''').select([likeKeyword, likeUploader, sort, size, page * size]); | ||
|
||
final totalCount = countPackagesQuery.first.values.first; | ||
|
||
final packages = foundPackagesQuery | ||
.map((row) => row['id'] as int) | ||
.map<UnpubPackage?>(queryPackageById) | ||
.whereType<UnpubPackage>(); | ||
|
||
return UnpubQueryResult(totalCount as int, packages.toList()); | ||
} | ||
|
||
int? getPackageId(String name) { | ||
return sqliteDatabase.prepare(''' | ||
select id from $packagesTableName | ||
where | ||
name = ? | ||
''').select([name]).firstOrNull?['id']; | ||
} | ||
|
||
UnpubPackage? queryPackageById(int id) { | ||
final packageRow = sqliteDatabase.prepare(''' | ||
select * from $packagesTableName | ||
where | ||
id = ? | ||
''').select([id]).firstOrNull; | ||
|
||
if (packageRow == null) return null; | ||
|
||
final versionsRows = sqliteDatabase.prepare(''' | ||
select * from $versionsTableName where package_id = ? | ||
''').select([id]); | ||
|
||
final versions = versionsRows.map((row) { | ||
final dataJson = UnpubVersion.fromJson(json.decode(row['data'], | ||
reviver: (key, value) => | ||
key == 'createdAt' ? DateTime.parse(value as String) : value)); | ||
return dataJson; | ||
}); | ||
|
||
return UnpubPackage( | ||
packageRow['name'], | ||
versions.toList(), | ||
true, | ||
versions.map((version) => version.uploader ?? '').toSet().toList(), | ||
DateTime.parse(packageRow['created_at']), | ||
DateTime.parse(packageRow['updated_at']), | ||
packageRow['downloads'], | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
library; | ||
|
||
export 'src/sqlite_meta_store.dart'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
name: unpub_sqlite | ||
description: Implementation of a unpub's `MetaStore` for a SQLite databases | ||
version: 0.1.0 | ||
repository: https://github.com/marianhlavac/unpub_sqlite | ||
|
||
environment: | ||
sdk: ^3.5.3 | ||
|
||
dependencies: | ||
unpub: ^2.1.0 | ||
sqlite3: ^2.5.0 |