Skip to content

Commit

Permalink
feat: Publish initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
marianhlavac committed Dec 15, 2024
0 parents commit 23ed473
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.dart_tool/
pubspec.lock
11 changes: 11 additions & 0 deletions .vscode/launch.json
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"
}
]
}
29 changes: 29 additions & 0 deletions README.md
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)
22 changes: 22 additions & 0 deletions example/unpub_sqlite_example.dart
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}');
}
7 changes: 7 additions & 0 deletions lib/src/exceptions.dart
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";
}
196 changes: 196 additions & 0 deletions lib/src/sqlite_meta_store.dart
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'],
);
}
}
3 changes: 3 additions & 0 deletions lib/unpub_sqlite.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
library;

export 'src/sqlite_meta_store.dart';
11 changes: 11 additions & 0 deletions pubspec.yaml
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

0 comments on commit 23ed473

Please sign in to comment.