From: Hamatoma Date: Thu, 14 Oct 2021 20:42:45 +0000 (+0200) Subject: Generator: SQL statement for list pages can be customized X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=5a3bac77f81b967fddde5810f969fd58c8df1c04;p=exhibition.git Generator: SQL statement for list pages can be customized * ListPageMetaData: ** new attributes: whereCondition, selectItems, joinItems, orderBy * module users: ** page list handles the filters now (fully generated) ** search button works * Generator: ** the generated code is now more idempotent by dart formatting --- diff --git a/lib/base/helper.dart b/lib/base/helper.dart index 23a13af..03aec4a 100644 --- a/lib/base/helper.dart +++ b/lib/base/helper.dart @@ -1,5 +1,19 @@ import 'defines.dart'; +/// Handles the [input] as a pattern in a filter list: +/// +/// Appends '*' if input does not end with a '*'. +/// Replaces the jokers '*' and '?' with the SQL equivalents '%' and '_'. +/// +/// Returns a SQL pattern string. +String asPattern(String input) { + final rc = input.isEmpty || input.endsWith('*') ? input : input + '*'; + return rc.replaceAll('*', '%').replaceAll('?', '_'); +} + +/// Converts a [name] to a camel case string. +/// +/// A camelCase string starts with a uppercase character. String toCamelCase(String name) { final rc = name.isEmpty ? '' : (name[0].toUpperCase() + name.substring(1)); return rc; diff --git a/lib/meta/module_meta_data.dart b/lib/meta/module_meta_data.dart index 68d9895..4959439 100644 --- a/lib/meta/module_meta_data.dart +++ b/lib/meta/module_meta_data.dart @@ -50,12 +50,50 @@ class FieldMetaData extends WidgetMetaData { class ListPageMetaData extends PageMetaData { final String tableHeaders; final String tableColumns; + final String whereCondition; + final String orderBy; + final String selectItems; + final String joinItems; + + /// Constructor. + /// + /// [name] is the page name. It must be unique over all pages in the module. + /// + /// [fields]: the field in the filter section. + /// + /// [tableColumns]: a semicolon delimited list of table columns used in the + /// table displaying the filtered records. Example: 'user_id;user_name;role' + /// + /// [tableHeaders]: an auto delimited list of headers for the table. Example: + /// ';Id;Name;Role'. Auto delimited: the first char defines the delimiter. + /// + /// [globalComboBoxes]: If there are filter combo boxes that can be constructed + /// by "global methods" the should be listed here. + /// Example: 'comboRoles;comboUsers' + /// + /// [whereCondition]: the filter condition in the SQL statement. Example: + /// '(:text IS NULL OR user_name like :text OR user_displayname like :text)' + /// + /// [orderBy]: the default order by clause. Example: 'changed desc,user_id' + /// + /// [selectItems]: additional select entries. Must end with ','. Example: + /// '''(SELECT count(*) FROM sessions WHERE sessions.user_id=t0.user_id) as count, + /// t1.role_created as roledate,''' + /// + /// [joinItems]: additional joins. Convention: use table ids different from + /// the created joins (t1, t2, ...), use j1, j2 ... + /// Example: '''JOIN sessions j1 ON j1.user_id=t0.user_id + /// JOIN logins j2 ON j2.user_id=t0.user_id''' ListPageMetaData(String label, {String name = '', required List fields, required this.tableColumns, required this.tableHeaders, - String globalComboBoxes = ''}) + String globalComboBoxes = '', + this.whereCondition = '', + this.orderBy = '', + this.selectItems = '', + this.joinItems = ''}) : super(label, PageType.list, name: name, fields: fields, globalComboBoxes: globalComboBoxes); } diff --git a/lib/meta/users_meta.dart b/lib/meta/users_meta.dart index 9b396d7..d6807b5 100644 --- a/lib/meta/users_meta.dart +++ b/lib/meta/users_meta.dart @@ -59,15 +59,20 @@ class UserMeta extends ModuleMetaData { ListPageMetaData( 'Users Overview', fields: [ - PropertyMetaData('text', i18n.tr('Text'), DataType.string, '', + PropertyMetaData( + 'text', i18n.tr('Text'), DataType.string, ':pattern:', size: 64), PropertyMetaData('role', i18n.tr('Role'), DataType.reference, '', displayType: DisplayType.combobox, foreignKey: 'roles.role_id;role_name;role'), ], - tableColumns: 'user_id;user_displayname;user_email;role', - tableHeaders: i18n.tr('Id;Display Name;Email;Role'), + tableColumns: 'user_id;user_name;user_displayname;user_email;role', + tableHeaders: i18n.tr(';Id;Name;Display Name;Email;Role'), globalComboBoxes: 'comboRoles', + whereCondition: '''(:text='' OR user_name like :text + OR user_displayname like :text OR user_email like :text) + AND (:role=0 OR :role=user_role)''', + orderBy: 'user_id', ), ]); @override diff --git a/lib/page/users/list_user_custom.dart b/lib/page/users/list_user_custom.dart index 195eb9a..4a62b31 100644 --- a/lib/page/users/list_user_custom.dart +++ b/lib/page/users/list_user_custom.dart @@ -2,11 +2,12 @@ // It will never overridden by the meta_tool. import 'package:flutter/material.dart'; +import '../../base/helper.dart'; import '../../base/i18n.dart'; +import '../../services/global_widget.dart'; import '../../setting/global_data.dart'; import '../../widget/attended_page.dart'; import '../../widget/widget_form.dart'; -import '../../services/global_widget.dart'; import 'list_user_page.dart'; final i18n = I18N(); @@ -63,14 +64,14 @@ class ListUserCustom extends State { screenWidth: attendedPage!.pageStates.screenWidth, padding: padding)))); final rows = attendedPage!.getRows( - columnList: 'user_id;user_displayname;user_email;role', + columnList: 'user_id;user_name;user_displayname;user_email;role', what: 'query', parameters: { 'module': 'Users', 'sql': 'list', 'offset': '0', 'size': '10', - ':text': _fieldData.text, + ':text': asPattern(_fieldData.text), ':role': _fieldData.role.toString(), }, onDone: () => setState(() => 1), @@ -81,6 +82,9 @@ class ListUserCustom extends State { DataColumn( label: Text(i18n.tr('Id')), ), + DataColumn( + label: Text(i18n.tr('Name')), + ), DataColumn( label: Text(i18n.tr('Display Name')), ), @@ -112,18 +116,23 @@ class ListUserCustom extends State { return rc; } - void search() { - //@ToDo + @override + void dispose() { + textController.dispose(); + super.dispose(); } + @override void initState() { super.initState(); } - @override - void dispose() { - textController.dispose(); - super.dispose(); + void search() { + attendedPage!.pageStates.dbDataState.clear(); + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + setState(() => 1); + } } } diff --git a/metatool/bin/page_generator.dart b/metatool/bin/page_generator.dart index 0d73fdb..79bc43e 100644 --- a/metatool/bin/page_generator.dart +++ b/metatool/bin/page_generator.dart @@ -54,11 +54,12 @@ class _EditUserPageState extends EditUserCustom { // It will never overridden by the meta_tool. import 'package:flutter/material.dart'; +import '../../base/helper.dart'; import '../../base/i18n.dart'; +import '../../services/global_widget.dart'; import '../../setting/global_data.dart'; import '../../widget/attended_page.dart'; import '../../widget/widget_form.dart'; -import '../../services/global_widget.dart'; import 'list_user_page.dart'; final i18n = I18N(); @@ -129,16 +130,22 @@ class ListUserCustom extends State { return rc; } - void search() { - //@ToDo + @override + void dispose() { +#DISPOSE_CONTROLLER super.dispose(); } + @override void initState() { super.initState(); } - @override - void dispose(){ -#DISPOSE_CONTROLLER super.dispose(); + + void search() { + attendedPage!.pageStates.dbDataState.clear(); + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + setState(() => 1); + } } } @@ -175,7 +182,8 @@ class EditUserCustom extends State with MessageLine { #INIT_COMBO#LOAD_RECORD#ASSIGN_CONTROLLER final formItems = [ #FORM_ITEMS FormItem( ElevatedButton( - onPressed: () => #ACTION2(), child: Text(i18n.tr('#BUTTON'))), + onPressed: () => #ACTION2(), + child: Text(i18n.tr('#BUTTON'))), weight: 8, gapAbove: 2 * padding), FormItem( @@ -204,7 +212,7 @@ class EditUserCustom extends State with MessageLine { formItems, screenWidth: attendedPage!.pageStates.screenWidth, padding: padding, - )))))); + )))))); return rc; } @@ -220,9 +228,7 @@ class EditUserCustom extends State with MessageLine { }; #SET_PRIMARY#CALL_TO_MAP globalData.restPersistence! .store(what: 'store', map: parameters) - .then((answer) { -#STORAGE_DONE - }); + .then((answer) {#STORAGE_DONE}); } void #ACTION2() { @@ -243,7 +249,9 @@ class _FieldData { #FROM_MAP#TO_MAP} '''; static final templateStorageDoneCreate = - ''' if (answer.startsWith('id:')) { + ''' + + if (answer.startsWith('id:')) { final id = int.tryParse(answer.substring(3)); if (id == null || id == 0) { setError(i18n.tr('Saving data failed: \$answer')); @@ -252,10 +260,11 @@ class _FieldData { attendedPage!.pageStates.dbDataState.clear(); globalData.navigate(context, '/#MODULE/edit;\$id'); } - }'''; + } + '''; static final templateStorageDoneEdit = ' setState(() => 1);'; static final templateStorageDoneDelete = - " globalData.navigate(context, '/#MODULE/list');"; + "\n globalData.navigate(context, '/#MODULE/list');\n "; static final templateFromMap = ''' void fromMap(Map map) { #BODY_FROM } '''; @@ -333,6 +342,9 @@ StatefulWidget? customPageByRoute(String route) { PageGenerator(BaseLogger logger) : super(logger); + /// Creates the part for initializing the [TextEditingController]s. + /// + /// "xxxController.text = _fieldData.yyy". String buildAssignControllers(PageMetaData page) { final buffer = StringBuffer(); for (var field in page.fields) { @@ -345,6 +357,9 @@ StatefulWidget? customPageByRoute(String route) { return buffer.toString(); } + /// Creates a button with [onPressed] and [label]. + /// + /// [indent]: the additional indention of each created line. String buildButton( {required String onPressed, required String label, String indent = ''}) { final rc = '''^ElevatedButton( @@ -356,6 +371,9 @@ StatefulWidget? customPageByRoute(String route) { return rc; } + /// Creates the text controllers of the [page]. + /// + /// Each text field get its own controller. String buildDefinitionControllers(PageMetaData page) { final buffer = StringBuffer(); for (var field in page.fields) { @@ -368,6 +386,7 @@ StatefulWidget? customPageByRoute(String route) { return buffer.toString(); } + /// Creates the field definitions of the [page] in the class _FieldData. String buildDefinitionFields(PageMetaData page) { final buffer = StringBuffer(); for (var field in page.fields) { @@ -418,6 +437,9 @@ StatefulWidget? customPageByRoute(String route) { return buffer.toString(); } + /// Creates the dispose statements of the [page] for the [TextEditingController]s. + /// + /// Each text field has its own controller. String buildDisposeControllers(PageMetaData page) { final buffer = StringBuffer(); for (var field in page.fields) { @@ -429,6 +451,11 @@ StatefulWidget? customPageByRoute(String route) { return buffer.toString(); } + /// Creates the widget of the [field] depending on the widget type. + /// + /// [indent]: the additional indention of each created line. + /// + /// [withController]: if true the text field uses a [TextEditingController]. String buildField(PropertyMetaData field, {String indent = '', bool withController = false}) { final name = field.name; @@ -482,6 +509,12 @@ StatefulWidget? customPageByRoute(String route) { return rc; } + /// Creates the form items of the [page]. + /// + /// A form item stores the data for creating automatically a multi column + /// form. + /// + /// [withController]: if true the text field uses a [TextEditingController]. String buildFormItems(PageMetaData page, {bool withController = false}) { final buffer = StringBuffer(); for (var field in page.fields) { @@ -505,6 +538,7 @@ StatefulWidget? customPageByRoute(String route) { return buffer.toString(); } + /// Creates the method "fromMap" in the class _FieldData for the [page]. String buildFromMap(PageMetaData page) { String? rc; if (page.pageType == PageType.edit || page.pageType == PageType.delete) { @@ -521,6 +555,7 @@ StatefulWidget? customPageByRoute(String route) { return rc ?? ''; } + /// Creates the part to initialize the combo boxes for the [page]. String buildInitializeComboBoxes(PageMetaData page) { final buffer = StringBuffer(); if (page.globalComboBoxes.isNotEmpty) { @@ -535,6 +570,7 @@ StatefulWidget? customPageByRoute(String route) { return buffer.toString(); } + /// Creates the part for loading the field data from the backend for [page]. String buildLoadRecord(PageMetaData page) { String? rc; if (page.pageType == PageType.edit || page.pageType == PageType.delete) { @@ -548,6 +584,9 @@ StatefulWidget? customPageByRoute(String route) { return rc ?? ''; } + /// Creates the parameter definition of the [page]. + /// + /// The parameters will be used for storing the record at the backend. String buildParamDefinitions(PageMetaData page, {bool withController = false}) { final buffer = StringBuffer(); @@ -557,25 +596,37 @@ StatefulWidget? customPageByRoute(String route) { (field as PropertyMetaData).displayType != DisplayType.text ? '.toString()' : ''; - buffer.writeln(" ':$name': _fieldData.$name$suffix,"); + var value = '_fieldData.$name$suffix'; + if (field is PropertyMetaData && field.hasOption(':pattern')){ + value = 'asPattern($value)'; + } + buffer.writeln(" ':$name': $value,"); } return buffer.toString(); } + /// Builds the table header of the [page]. String buildTableHeader(ListPageMetaData page) { final buffer = StringBuffer(); - for (var item in page.tableHeaders.split(';')) { - buffer.writeln(''' DataColumn( + final headers = page.tableHeaders; + if (headers.length < 2) { + logger.error('tableHeaders is too short: $headers'); + } else { + // tableHeaders is auto delimited: first char defines the delimiter + for (var item in headers.substring(1).split(headers[0])) { + buffer.writeln(''' DataColumn( label: Text(i18n.tr('$item')), ),'''); + } } return buffer.toString(); } + /// Creates the method "toMap" in class _FieldData of the [page]. String buildToMap(PageMetaData page) { String? rc; if (page.pageType == PageType.edit || page.pageType == PageType.create) { - final buffer = StringBuffer(' void toMap(Map map) {\n'); + final buffer = StringBuffer('\n void toMap(Map map) {\n'); if (page.pageType == PageType.edit) { buffer.writeln(" // please set outside: map[':id'] = primaryKey;"); } diff --git a/metatool/bin/sql_generator.dart b/metatool/bin/sql_generator.dart index ecb791f..208f2fc 100644 --- a/metatool/bin/sql_generator.dart +++ b/metatool/bin/sql_generator.dart @@ -32,6 +32,58 @@ class SqlGenerator extends GeneratorBase { return buffer.toString(); } + /// Creates the sections for pages with PageType.list of the [module]. + String createListSections(ModuleMetaData module) { + final buffer = StringBuffer(); + List? listPages; + try { + listPages = module.pageList + .where((element) => element.pageType == PageType.list) + .toList(growable: false); + } on Exception catch (exc) { + logger.error('no page with list type: $exc'); + listPages = null; + } + if (listPages != null) { + final tableName = module.tableName; + for (var page in listPages) { + if (page is ListPageMetaData) { + var whereCondition = page.whereCondition; + if (whereCondition.endsWith('\n')){ + whereCondition = whereCondition.substring(0, whereCondition.length - 1); + } + String selectItems2 = ''; + var selectItems = + page.selectItems.isEmpty ? '' : indent(page.selectItems, ' '); + var joins = findReferences(module, (x) => selectItems2 = x); + if (page.joinItems.isNotEmpty) { + joins += indent(page.joinItems, ' '); + if (!joins.endsWith('\n')) { + joins += '\n'; + } + } + var parameters = ''; + if (whereCondition.isNotEmpty) { + parameters = findParameters(whereCondition, page.fields); + whereCondition = ' WHERE\n' + indent(whereCondition, ' '); + } + final order = page.orderBy.isNotEmpty + ? page.orderBy + : module.primaryOf()!.columnName; + buffer.writeln(page.name + ':'); + buffer.writeln(''' type: list + parameters: [$parameters] + order: "$order" + sql: "SELECT +$selectItems t0.*$selectItems2 + FROM $tableName t0 +$joins$whereCondition ;"'''); + } + } + } + return buffer.toString(); + } + /// Returns the SQL statements for insert, update, delete... /// for a given [module]. /// This yaml file is a configuration for the rest_server. @@ -41,18 +93,11 @@ class SqlGenerator extends GeneratorBase { final list = module.propertyList; final buffer = StringBuffer(); buffer.write('---\n'); + final lists = createListSections(module); buffer.write('# DO NOT CHANGE. This file is created by the meta_tool\n'); var sqlText = '''# SQL statements of the module "$moduleName":\n module: $moduleName -list: - type: list - parameters: [] - sql: "SELECT - t0.*SELECTS - FROM $tableName t0 -JOINS - ;" -byId: +${lists}byId: type: record parameters: [ ":${list[0].name}" ] sql: "SELECT * FROM $tableName WHERE ${list[0].columnName}=:${list[0].name};" @@ -63,7 +108,6 @@ delete: update: type: update '''; - sqlText = handleReferences(sqlText, module); buffer.write(sqlText); var items = module.standardColumns('changedBy'); var parameters = addToBuffer(' parameters: [', maxLength: 80); @@ -139,13 +183,21 @@ update: buffer.writeln(sql2); return buffer.toString(); } + static final regExprParameters = RegExp(r'(:\w+)'); + /// Searches the parameters in the [whereCondition] and checks it against + /// the [fields]. + String findParameters(String whereCondition, List fields) { + final names = regExprParameters.allMatches(whereCondition) + .map((element) => element.group(1)) + .toSet().toList(); + return '"' + names.join('","') + '"'; + } - /// Handles the foreign keys in the [sqlText] of the given [module]. - /// - /// Replaces the placeholders SELECTS and JOINS in [sqlText]. + /// Creates the joins and select items the foreign keys of the given [module]. /// - /// Returns the modified SQL text. - String handleReferences(String sqlText, ModuleMetaData module) { + /// [storeSelectItems]: a function that stores the select items to the caller. + String findReferences(ModuleMetaData module, + String Function(String selectItems) storeSelectItems) { var joins = ''; var selects = ''; var referenceNo = 0; @@ -164,14 +216,21 @@ update: } else { ++referenceNo; joins += ' JOIN ${keyParts[0]} t$referenceNo ON ' - 't$referenceNo.${keyParts[1]}=t0.${property.columnName}'; + 't$referenceNo.${keyParts[1]}=t0.${property.columnName}\n'; selects += ',t$referenceNo.${parts[1]} AS ${parts[2]}'; } } } } - final rc = - sqlText.replaceFirst('JOINS', joins).replaceFirst('SELECTS', selects); + storeSelectItems(selects); + return joins; + } + + /// Adds to all lines in the string [lines] a given [indentString]. + String indent(String lines, String indentString) { + final lines2 = lines.split('\n'); + final rc = lines2.fold( + '', (previous, element) => previous + indentString + element + '\n'); return rc; } diff --git a/rest_server/data/sql/roles.sql.yaml b/rest_server/data/sql/roles.sql.yaml index 616389e..1eded90 100644 --- a/rest_server/data/sql/roles.sql.yaml +++ b/rest_server/data/sql/roles.sql.yaml @@ -6,10 +6,10 @@ module: Roles list: type: list parameters: [] + order: "role_id" sql: "SELECT t0.* FROM roles t0 - ;" byId: type: record diff --git a/rest_server/data/sql/structures.sql.yaml b/rest_server/data/sql/structures.sql.yaml index 111f673..9595e1d 100644 --- a/rest_server/data/sql/structures.sql.yaml +++ b/rest_server/data/sql/structures.sql.yaml @@ -6,10 +6,10 @@ module: Structures list: type: list parameters: [] + order: "structure_id" sql: "SELECT t0.* FROM structures t0 - ;" byId: type: record diff --git a/rest_server/data/sql/users.sql.yaml b/rest_server/data/sql/users.sql.yaml index 7050e27..5102dfe 100644 --- a/rest_server/data/sql/users.sql.yaml +++ b/rest_server/data/sql/users.sql.yaml @@ -5,11 +5,16 @@ module: Users list: type: list - parameters: [] + parameters: [":text",":role"] + order: "user_id" sql: "SELECT t0.*,t1.role_name AS role FROM users t0 JOIN roles t1 ON t1.role_id=t0.user_role + WHERE + (:text='' OR user_name like :text + OR user_displayname like :text OR user_email like :text) + AND (:role=0 OR :role=user_role) ;" byId: type: record