From: Hamatoma Date: Tue, 20 Jul 2021 15:04:10 +0000 (+0200) Subject: rest_server, rest_client X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=4007aa2e63d7c9eb765512138cf405ac21b3ae00;p=exhibition.git rest_server, rest_client * rest_server and rest_client work now * unittest: rest_server_test --- diff --git a/lib/persistence/persistence.dart b/lib/persistence/persistence.dart index ec23a68..2f847d0 100644 --- a/lib/persistence/persistence.dart +++ b/lib/persistence/persistence.dart @@ -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 instance). - Future query({required String what, Map? parameters}); + Future query({required String what, Map? data}); /// Executes a SQL statement with not or a very short result: /// insert: answers the primary key. diff --git a/lib/persistence/rest_persistence.dart b/lib/persistence/rest_persistence.dart index 5ec3d77..d94597c 100644 --- a/lib/persistence/rest_persistence.dart +++ b/lib/persistence/rest_persistence.dart @@ -88,9 +88,9 @@ class RestPersistence extends Persistence { @override Future query( - {required String what, Map? parameters}) async { + {required String what, Map? 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 index 0000000..0652fce --- /dev/null +++ b/rest_client/lib/persistence @@ -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 index 0000000..54aa7c0 --- /dev/null +++ b/rest_client/lib/setting/error_handler_exhibition.dart @@ -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 index 0000000..c7c4f54 --- /dev/null +++ b/rest_client/pubspec.yaml @@ -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 diff --git a/rest_client/test/rest_client_test.dart b/rest_client/test/rest_client_test.dart index 6a79c3b..6442bbe 100644 --- a/rest_client/test/rest_client_test.dart +++ b/rest_client/test/rest_client_test.dart @@ -25,27 +25,81 @@ void main() async { final client = RestPersistence.fromConfig(configuration, logger); group('query', () { Map result; - Map 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 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 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'); + }); }); } diff --git a/rest_server/bin/rest_server_test.dart b/rest_server/bin/rest_server_test.dart index 12cd6d3..cfef8ea 100644 --- a/rest_server/bin/rest_server_test.dart +++ b/rest_server/bin/rest_server_test.dart @@ -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'); } } } diff --git a/rest_server/lib/rest_server.dart b/rest_server/lib/rest_server.dart index 0236752..f59e323 100644 --- a/rest_server/lib/rest_server.dart +++ b/rest_server/lib/rest_server.dart @@ -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 names, Map 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 checkSession(Map 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 getSessionId(Map 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 getToken(Map 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 ''; } /// 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'); diff --git a/rest_server/lib/sql_storage.dart b/rest_server/lib/sql_storage.dart index 18c9c00..5429928 100644 --- a/rest_server/lib/sql_storage.dart +++ b/rest_server/lib/sql_storage.dart @@ -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). diff --git a/rest_server/pubspec.yaml b/rest_server/pubspec.yaml index c7c4f54..fccd1f9 100644 --- a/rest_server/pubspec.yaml +++ b/rest_server/pubspec.yaml @@ -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 diff --git a/rest_server/test/sql_storage_test.dart b/rest_server/test/sql_storage_test.dart index d47f6c3..fab82bf 100644 --- a/rest_server/test/sql_storage_test.dart +++ b/rest_server/test/sql_storage_test.dart @@ -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);