]> gitweb.hamatoma.de Git - exhibition.git/commitdiff
rest_server, rest_client
authorHamatoma <author.hamatoma.de>
Tue, 20 Jul 2021 15:04:10 +0000 (17:04 +0200)
committerHamatoma <author.hamatoma.de>
Tue, 20 Jul 2021 15:04:10 +0000 (17:04 +0200)
* rest_server and rest_client work now
* unittest: rest_server_test

lib/persistence/persistence.dart
lib/persistence/rest_persistence.dart
rest_client/lib/persistence [new symlink]
rest_client/lib/setting/error_handler_exhibition.dart [new symlink]
rest_client/pubspec.yaml [new file with mode: 0644]
rest_client/test/rest_client_test.dart
rest_server/bin/rest_server_test.dart
rest_server/lib/rest_server.dart
rest_server/lib/sql_storage.dart
rest_server/pubspec.yaml
rest_server/test/sql_storage_test.dart

index ec23a6890ddaa59d7b8fd91a6a86ff7603b08132..2f847d0c1ec9d47032c3ecd7293098b21276e474 100644 (file)
@@ -1,8 +1,8 @@
 abstract class Persistence {
-  /// Fetches data specified by [what] and some [parameters].
+  /// Fetches data specified by [what] and some [data].
   /// Returns null or a record (as a Map instance)
   /// or a list of records (as a List<Map> instance).
-  Future<dynamic> query({required String what, Map<String, dynamic>? parameters});
+  Future<dynamic> query({required String what, Map<String, dynamic>? data});
 
   /// Executes a SQL statement with not or a very short result:
   /// insert: answers the primary key.
index 5ec3d7764063803ded94b32b14d8adc0f574add1..d94597cb5a2067dd37825f4dd59ed074e211e7ec 100644 (file)
@@ -88,9 +88,9 @@ class RestPersistence extends Persistence {
 
   @override
   Future<dynamic> query(
-      {required String what, Map<String, dynamic>? parameters}) async {
+      {required String what, Map<String, dynamic>? data}) async {
     var rc;
-    final params2 = parameters == null ? '{}' : convert.jsonEncode(parameters);
+    final params2 = data == null ? '{}' : convert.jsonEncode(data);
     final answer = await runRequest(what, body: params2, headers: jsonHeader);
     if (answer.isNotEmpty && (answer.startsWith('{') || answer.startsWith('['))) {
       rc = convert.jsonDecode(answer);
diff --git a/rest_client/lib/persistence b/rest_client/lib/persistence
new file mode 120000 (symlink)
index 0000000..0652fce
--- /dev/null
@@ -0,0 +1 @@
+../../lib/persistence/
\ No newline at end of file
diff --git a/rest_client/lib/setting/error_handler_exhibition.dart b/rest_client/lib/setting/error_handler_exhibition.dart
new file mode 120000 (symlink)
index 0000000..54aa7c0
--- /dev/null
@@ -0,0 +1 @@
+../../../lib/setting/error_handler_exhibition.dart
\ No newline at end of file
diff --git a/rest_client/pubspec.yaml b/rest_client/pubspec.yaml
new file mode 100644 (file)
index 0000000..c7c4f54
--- /dev/null
@@ -0,0 +1,18 @@
+name: rest_server
+description: A REST server to answer client database requests.
+version: 0.1.0
+# homepage: https://www.example.com
+
+environment:
+  sdk: '>=2.12.0 <3.0.0'
+
+dependencies:
+  http: ^0.13.0
+  args: ^2.1.0
+  path: ^1.8.0
+  yaml: ^3.1.0
+  dart_bones: ^1.1.1
+
+dev_dependencies:
+  lints: ^1.0.0
+  test: ^1.16.0
index 6a79c3b314a9e6534d60a2cc9169105d9c97e7eb..6442bbe862f17d4f4d940bd7b12a1623a45ad292 100644 (file)
@@ -25,27 +25,81 @@ void main() async {
   final client = RestPersistence.fromConfig(configuration, logger);
   group('query', () {
     Map result;
-    Map<String, String> parameters;
-    test('record', () async {
-      parameters = {'module': 'Persons', 'sql': 'byId', ':id': '13'};
-      var result = await client.query(what: 'query', parameters: parameters);
-      expect(result, isNotNull);
-      expect(result, isNotEmpty);
-      expect(result['person_id'], 13);
-    });
+    Map<String, String> data;
     test('list', () async {
-      parameters = {'module': 'Persons', 'sql': 'list'};
-      var result = await client.query(what: 'query', parameters: parameters);
+      data = {'module': 'Persons', 'sql': 'list'};
+      var result = await client.query(what: 'query', data: data);
       expect(result, isNotNull);
       expect(result is Iterable, isTrue);
       expect(result.length, greaterThan(2));
-      final record = result[0];
+      var record = result[0];
+      expect(record['person_id'], 11);
+      expect(record['person_name'], 'Jones');
+      record = result[1];
+      expect(record['person_id'], 12);
+      expect(record['person_name'], 'Miller');
+      record = result[2];
       expect(record['person_id'], 13);
+      expect(record['person_name'], 'Smith');
+    });
+    test('record', () async {
+      data = {'module': 'Persons', 'sql': 'byId', ':id': '13'};
+      var result = await client.query(what: 'query', data: data);
+      expect(result, isNotNull);
+      expect(result, isNotEmpty);
+      expect(result['person_id'], 13);
     });
   });
   group('store', () {
     Map<String, dynamic> parameters;
-    parameters = { };
-    //client.store(what: 'store', data: parameters);
+    int id = 0;
+    test('insert', () async {
+      parameters = {
+        'module': 'Persons',
+        'sql': 'insert',
+        ':name': 'Mozart',
+        ':email': 'mozart@salzburg.au',
+        ':createdby': 'unittest'
+      };
+      var result = await client.store(what: 'store', data: parameters);
+      expect(result, isNotNull);
+      final match = RegExp(r'^id:(\d+)$').firstMatch(result);
+      expect(match, isNotNull);
+      id = int.parse(match!.group(1)!);
+    });
+    test('update', () async {
+      parameters = {
+        'module': 'Persons',
+        'sql': 'update',
+        ':id': id.toString(),
+        ':name': 'Bach',
+        ':email': 'bach@wien.at',
+        ':changedby': 'unittest'
+      };
+      var result = await client.store(what: 'store', data: parameters);
+      expect(result, isNotNull);
+      final match = RegExp(r'^rows:(\d+)$').firstMatch(result);
+      expect(match, isNotNull);
+      expect(match!.group(1)!, '1');
+      final data2 = {'module': 'Persons', 'sql': 'byId', ':id': id.toString()};
+      var result2 = await client.query(what: 'query', data: data2);
+      expect(result2, isNotNull);
+      expect(result2, isNotEmpty);
+      expect(result2['person_id'], id);
+      expect(result2['person_name'], 'Bach');
+      expect(result2['person_email'], 'bach@wien.at');
+    });
+    test('delete', () async {
+      parameters = {
+        'module': 'Persons',
+        'sql': 'delete',
+        ':id': id.toString(),
+      };
+      var result = await client.store(what: 'store', data: parameters);
+      expect(result, isNotNull);
+      final match = RegExp(r'^rows:(\d+)$').firstMatch(result);
+      expect(match, isNotNull);
+      expect(match!.group(1), '1');
+    });
   });
 }
index 12cd6d36f39d66a4d8228d7bb154eb51b0c3eb5b..cfef8eac4334f75e34608dbb9cdca78adb46bb58 100644 (file)
@@ -42,6 +42,7 @@ db:
   code: "TopSecret"
   host: localhost
   port: 3306
+  primaryTable: persons
   timeout: 30
   traceDataLength: 200
 clientSessionTimeout: 900
@@ -69,8 +70,8 @@ update:
 insert:
   type: insert
   parameters: [":name", ":email", ":createdby"]
-  sql: "INSERT INTO persons(person_name, person_email, person_changedby)
-    VALUES(:name, :displayname, :email, NOW(), :createdby);"
+  sql: "INSERT INTO persons(person_name, person_email, person_created, person_createdby)
+    VALUES(:name, :email, NOW(), :createdby);"
 delete:
   type: delete
   parameters: [':id']
@@ -110,10 +111,11 @@ class TestDbInitializer {
   person_created timestamp NULL,
   person_createdby varchar(16),
   person_changed timestamp NULL,
+  person_changedby varchar(16),
   PRIMARY KEY (person_id)
 );
 ''';
-      if (!await db.execute(sql)) {
+      if (await db.execute(sql) < 0) {
         logger.error('cannot create persons');
       }
     }
@@ -125,8 +127,8 @@ class TestDbInitializer {
 (12, 'Miller', 'miller@hamatoma.de', NOW(), 'daemon'),
 (13, 'Smith', 'smith@hamatoma.de', NOW(), 'daemon');
 ''';
-      if (!await db.execute(sql)) {
-        logger.error('cannot create apiaries records');
+      if (await db.execute(sql) < 0) {
+        logger.error('cannot create persons records');
       }
     }
   }
index 0236752f5b16a847c1f8300feef9cd0b08a0683f..f59e3230c375cc8bb41253660478effa95b05ada 100644 (file)
@@ -9,6 +9,7 @@ import 'package:dart_bones/dart_bones.dart';
 import 'package:http/http.dart' as http;
 import 'package:path/path.dart' as path;
 import 'package:rest_server/sql_storage.dart';
+import 'package:mysql1/mysql1.dart';
 
 const forbidden = 'forbidden';
 const wrongData = 'wrong data';
@@ -62,6 +63,7 @@ class RestServer {
     String dbUser = 'exhibition',
     String dbCode = 'TopSecret',
     String dbHost = 'localhost',
+    String dbPrimaryTable = 'users',
     int dbPort = 3306,
     int watchDogPause = 60,
     int traceDataLength = 80,
@@ -91,15 +93,15 @@ class RestServer {
         'code': dbCode,
         'host': dbHost,
         'port': dbPort,
+        'primaryTable': dbPrimaryTable,
         'traceDataLength': traceDataLength,
       },
       'clientSessionTimeout': clientSessionTimeout,
     }, logger);
   }
 
-  /// Konstruktor, der die Parameter aus der YAML-Konfigurationsdatei
-  /// [filename] bezieht.
-  ///
+  /// Constructor using parameters defined in the YAML configuration named
+  /// [filename].
   RestServer.fromConfig(String filename, {this.serviceName = 'exhibition'}) {
     final logger2 = unittestLogger ?? MemoryLogger();
     configuration = Configuration.fromFile(filename, logger2);
@@ -258,12 +260,16 @@ class ServiceWorker {
   String restVersion = '';
   FileSync? _fileSync = FileSync();
   SqlStorage sqlStorage = SqlStorage(globalLogger);
+  String sqlTestPrimaryTable = '';
+
   ServiceWorker(this.threadId, this.configuration, this.serviceName) {
     final fnLog = '/var/log/local/$serviceName.$threadId.log';
     logger = RestServer.unittestLogger ??
         Logger(fnLog,
             configuration.asInt('logLevel', section: 'service') ?? LEVEL_FINE);
     logger.log('db: ${configuration.asString('db', section: 'db')}');
+    final table = configuration.asString('primaryTable', section: 'db');
+    sqlTestPrimaryTable = 'select count(*) from $table;';
     db = MySqlDb.fromConfiguration(configuration, logger);
     clientSessionTimeout = configuration.asInt('clientSessionTimeout') ?? 30;
     _fileSync = FileSync(logger);
@@ -274,25 +280,32 @@ class ServiceWorker {
   /// We cannot use jsonDecode(): data types like DateTime are not supported.
   StringBuffer arrayToJson(Iterable list, StringBuffer buffer) {
     buffer.write('[');
+    String? separator;
     for (var item in list) {
-      if (item is Iterable) {
-        arrayToJson(item, buffer);
+      if (separator == null){
+        separator = ',';
+      } else {
+        buffer.write(separator);
+      }
+      if (item is ResultRow) {
+        rowToJson(item.fields, buffer);
       } else if (item is Map) {
         rowToJson(item, buffer);
+      } else if (item is Iterable) {
+          arrayToJson(item, buffer);
       }
     }
     buffer.write(']');
     return buffer;
   }
 
-  /// Checks whether a valid connection is available. If not a reconnection is
-  /// done.
+  /// Checks whether a valid connection is available.
+  /// If not a reconnection is done.
   Future checkConnection() async {
     var ready = false;
-    final sql = 'select count(*) from loginusers;';
     int? count;
     try {
-      count = await db!.readOneInt(sql);
+      count = await db!.readOneInt(sqlTestPrimaryTable);
     } catch (exc) {
       logger.error(exc.toString());
       ready = true;
@@ -304,6 +317,8 @@ class ServiceWorker {
     }
   }
 
+  /// Tests wheter all [names] are keys in [parameters].
+  /// @throws FormatException on error.
   void checkParameters(List<String> names, Map<String, dynamic> parameters) {
     for (var name in names) {
       if (!parameters.containsKey(name)) {
@@ -312,38 +327,10 @@ class ServiceWorker {
     }
   }
 
-  /// Prüft die Session-ID.
-  /// Liefert true, wenn OK, false sonst.
+  /// Checks the validity of the session id.
+  /// Not implemented yet.
   Future<bool> checkSession(Map<String, dynamic> parameters) async {
-    var rc = false;
-    if (parameters.containsKey('sessionid')) {
-      final id = parameters['sessionid'];
-      final sql = '''SELECT
-  connection_id, connection_apiaristid, connection_start
-FROM connections
-WHERE
-  connection_name=?
-  AND connection_start >= NOW() - INTERVAL $clientSessionTimeout SECOND
-;
-''';
-      final record = await db!.readOneAsMap(sql, params: [id]);
-      if (record == null) {
-        logger.log('ungültige Session: $id', LEVEL_DETAIL);
-      } else {
-        logger.log(
-            'Session: $id apiarist: ${record['connection_apiaristid']} start: ${record['connection_start']}',
-            LEVEL_DETAIL);
-        rc = true;
-        final sql2 = '''UPDATE connections SET 
-  connection_requests=connection_requests+1,
-  connection_end=NOW()
-WHERE
-  connection_name=?
-;
-''';
-        await db!.updateOne(sql2, params: [id]);
-      }
-    }
+    var rc = true;
     return rc;
   }
 
@@ -387,101 +374,17 @@ WHERE
     return rc;
   }
 
-  /// Bearbeitet die Anforderung 'sessionid':
-  /// Prüfen der Lizenz ("Token"). Wenn erfolgreich, wird die Session
-  /// in die DB eingetragen.
-  /// Liefert eine JSon-Map (als String) oder null, wenn Fehler.
+  /// Handles the request "sessionId".
+  /// Not implemented yet.
   Future<String?> getSessionId(Map<String, dynamic> parameters) async {
     String? rc;
-    var sql = '''SELECT apiarist_id
-FROM apiarists
-WHERE apiarist_token=?; 
-''';
-    final token = parameters['token'];
-    final params = [token];
-    final apiaristId = await db!.readOneInt(sql, params: params);
-    if (apiaristId == null) {
-      logger.log('unknown token: $token', LEVEL_DETAIL);
-    } else {
-      sql = '''INSERT
- INTO connections
-  (connection_name, connection_apiaristid, connection_start, connection_end, connection_requests)
- VALUES (?, ?, NOW(), NOW(), 0);
- ''';
-
-      /// Use only 31 bit (non negativ numbers on 32 bit clients):
-      final sessionId = int.parse(
-              buildMd5Hash(
-                      token + DateTime.now().microsecondsSinceEpoch.toString())
-                  .substring(0, 8),
-              radix: 16) &
-          0x7fffffff;
-      final params2 = [sessionId, apiaristId];
-      final id = await db!.insertOne(sql, params: params2);
-      if (id <= 0) {
-        logger.error('insert into connection failed: $token');
-      } else {
-        logger.log('sessionid: $sessionId', LEVEL_DETAIL);
-        rc = convert.jsonEncode({'sessionid': sessionId});
-      }
-    }
     return rc;
   }
 
-  /// Liefert zu einem [name] das SQL-Statement.
-  /// [paramCount] ist die Anzahl der Parameter. Dient zur Konsistenzprüfung.
-  /// Liefert null oder das SQL-Statement
-  String? getSql(String name, int paramCount) {
-    String? sql;
-    var expectedCount = 0;
-    switch (name) {
-      case 'hives':
-        sql = '''SELECT
- *
-FROM hives
-WHERE hive_apiaryid=:id    
-''';
-        expectedCount = 1;
-        break;
-      case 'apiaristByToken':
-        sql = '''SELECT
-  *
-FROM apiary
-WHERE
-  apiarist_token=:token
-''';
-        expectedCount = 1;
-        break;
-      default:
-        logger.error('unknown SQL name: $name');
-        break;
-    }
-    if (expectedCount != paramCount) {
-      logger.error(
-          'unexpected parameter count in $name: $paramCount instead of $expectedCount');
-    }
-    return sql;
-  }
-
-  /// Bearbeitet die Anforderung 'register':
-  /// Test, ob der Name in den Parametern bei den Imkern existiert.
-  /// Wenn ja, wird der Token geliefert, sonst null.
+  /// Handles the request 'register':
+  /// not implemented yet.
   Future<String> getToken(Map<String, dynamic> parameters) async {
-    String rc;
-    final sql = '''SELECT apiarist_token
-FROM apiarists
-WHERE apiarist_registername=?; 
-''';
-    final name = parameters['name'];
-    final params = [name];
-    final token = await db!.readOneString(sql, params: params);
-    if (token == null) {
-      logger.log('unknown name: $name', LEVEL_DETAIL);
-      rc = wrongData;
-    } else {
-      rc = convert.jsonEncode({'token': token});
-    }
-    return rc;
+    return '<not implemented yet>';
   }
 
   /// Handles a POST request.
@@ -649,10 +552,14 @@ WHERE apiarist_registername=?;
     final sql = sqlStatement.sqlStatement(parameters, positionalParameters);
     if (sqlStatement.type == SqlStatementType.record) {
       final record = await db!.readOneAsMap(sql, params: positionalParameters);
-      rc = record == null ? 'NONE' : rowToJson(record, StringBuffer()).toString();
+      rc = record == null
+          ? 'NONE'
+          : rowToJson(record, StringBuffer()).toString();
     } else {
       final records = await db!.readAll(sql, params: positionalParameters);
-      rc = records == null ? 'NONE' : arrayToJson(records, StringBuffer()).toString();
+      rc = records == null
+          ? 'NONE'
+          : arrayToJson(records, StringBuffer()).toString();
     }
     return rc;
   }
@@ -756,8 +663,12 @@ WHERE apiarist_registername=?;
       final sql = sqlStatement.sqlStatement(parameters, positionalParameters);
       switch (sqlStatement.type) {
         case SqlStatementType.execute:
+          final count = await db!.execute(sql, params: positionalParameters);
+          rc = count < 0 ? 'ERROR' : 'rows:$count';
+          break;
         case SqlStatementType.delete:
-          await db!.execute(sql, params: positionalParameters);
+          final count = await db!.deleteRaw(sql, params: positionalParameters);
+          rc = count < 0 ? 'ERROR' : 'rows:$count';
           break;
         case SqlStatementType.insert:
           final id = await db!.insertOne(sql, params: positionalParameters);
@@ -771,9 +682,9 @@ WHERE apiarist_registername=?;
           }
           break;
         case SqlStatementType.update:
-          final success =
-              await db!.updateOne(sql, params: positionalParameters);
-          rc = success ? 'OK' : 'FAILED';
+          final count =
+              await db!.updateRaw(sql, params: positionalParameters);
+          rc = 'rows:$count';
           break;
         default:
           logger.error('unexpected type ${sqlStatement.type} in storeBySql');
@@ -879,7 +790,7 @@ class WorkerParameters {
     const baseService = 2;
     const baseTrace = baseService + 5;
     const baseDb = baseTrace + 1;
-    const baseRest = baseDb + 7;
+    const baseRest = baseDb + 8;
     final configuration = BaseConfiguration({
       'service': {
         'address': parts[baseService],
@@ -899,6 +810,7 @@ class WorkerParameters {
         'port': int.parse(parts[baseDb + 4]),
         'timeout': int.parse(parts[baseDb + 5]),
         'traceDataLength': int.parse(parts[baseDb + 6]),
+        'primaryTable': parts[baseDb + 7],
       },
       'clientSessionTimeout': int.parse(parts[baseRest]),
     }, MemoryLogger());
@@ -936,6 +848,7 @@ class WorkerParameters {
       configuration
           .asInt('traceDataLength', section: section3, defaultValue: 3306)
           .toString(),
+      configuration.asString('primaryTable', section: section3) ?? 'users',
       configuration.asInt('clientSessionTimeout', defaultValue: 900).toString(),
       serviceName,
     ].join('\t');
index 18c9c00186bdb6d5da24c2e3a8d30eb2ce4e3feb..542992872d131b237b9660c440699f5e467d3cf2 100644 (file)
@@ -59,16 +59,23 @@ class SqlStatement {
   SqlStatement(
       this.name, this.parameters, this.sql, this.type, this.sqlModule) {
     var parameters2 = parameters.toList();
-    parameters2.sort();
-    // revers sorting: if a member is a prefix of another member then it is
-    // positioned behind: example [':id1', ':id' ]
-    // The longer member is found first.
-    parameters2 = parameters2.reversed.toList();
-    RegExp regExp = RegExp('(' + parameters2.join('|') + r')\b');
-    for (var match in regExp.allMatches(sql)) {
-      orderOfParameters.add(match.group(0)!);
+    if (parameters.isEmpty) {
+      sqlPrepared = sql;
+    } else {
+      parameters2.sort();
+      // revers sorting: if a member is a prefix of another member then it is
+      // positioned behind: example [':id1', ':id' ]
+      // The longer member is found first.
+      parameters2 = parameters2.reversed.toList();
+      RegExp regExp = RegExp('(' + parameters2.join('|') + r')\b');
+      for (var match in regExp.allMatches(sql)) {
+        final name = match.group(0)!;
+        if (name.isNotEmpty) {
+          orderOfParameters.add(name);
+        }
+      }
+      sqlPrepared = sql.replaceAll(regExp, '?');
     }
-    sqlPrepared = sql.replaceAll(regExp, '?');
   }
 
   /// Returns a SQL statement and a parameter list (positional parameters).
index c7c4f5473b7f65aa820289b17c60682f1cf0a7bf..fccd1f914f2fb9cb14627441b401c8c8b3caff73 100644 (file)
@@ -11,7 +11,7 @@ dependencies:
   args: ^2.1.0
   path: ^1.8.0
   yaml: ^3.1.0
-  dart_bones: ^1.1.1
+  dart_bones: ^1.2.1
 
 dev_dependencies:
   lints: ^1.0.0
index d47f6c379df40a46f3aa8bb45486a06cf1aceca5..fab82bf87428ec98c5045171c306a067967b36ce 100644 (file)
@@ -1,6 +1,6 @@
-import 'package:test/test.dart';
-import 'package:rest_server/sql_storage.dart';
 import 'package:dart_bones/dart_bones.dart';
+import 'package:rest_server/sql_storage.dart';
+import 'package:test/test.dart';
 
 void main() {
   final logger = MemoryLogger(LEVEL_DETAIL);
@@ -10,7 +10,10 @@ void main() {
       final sqlStorage = SqlStorage(logger);
       //print('current directory: ${Directory.current.path}');
       sqlStorage.read('data/sql');
-      expect(sqlStorage.sqlStatement('Users', 'list'), isNotNull);
+      SqlStatement list;
+      expect((list = sqlStorage.sqlStatement('Users', 'list')), isNotNull);
+      expect(list.orderOfParameters, []);
+      expect(list.sqlPrepared, 'select * from loginusers;');
       expect(sqlStorage.sqlStatement('Users', 'byId'), isNotNull);
       SqlStatement update;
       expect(update = sqlStorage.sqlStatement('Users', 'update'), isNotNull);