]> gitweb.hamatoma.de Git - exhibition.git/commitdiff
Generator: SQL statement for list pages can be customized
authorHamatoma <author.hamatoma.de>
Thu, 14 Oct 2021 20:42:45 +0000 (22:42 +0200)
committerHamatoma <author.hamatoma.de>
Thu, 14 Oct 2021 20:42:45 +0000 (22:42 +0200)
* 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

lib/base/helper.dart
lib/meta/module_meta_data.dart
lib/meta/users_meta.dart
lib/page/users/list_user_custom.dart
metatool/bin/page_generator.dart
metatool/bin/sql_generator.dart
rest_server/data/sql/roles.sql.yaml
rest_server/data/sql/structures.sql.yaml
rest_server/data/sql/users.sql.yaml

index 23a13af58f1ff3a2a606b57d56c629d9d65891f6..03aec4a393bcd5793f3caf4a43006ba94d2676c1 100644 (file)
@@ -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;
index 68d98956c979180254d7bf860e239ae8d3de4f03..4959439f23027423bc1439d019524c110ec4211a 100644 (file)
@@ -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<WidgetMetaData> 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);
 }
index 9b396d7b20eb457f20ded0aa244769a4660a3d18..d6807b5844157d26b5cfbca55bbe7d7514e2e85d 100644 (file)
@@ -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
index 195eb9afb19ba0bd2ae779a0ac5b35c520d5d1d0..4a62b31860afc2f49553c0413bda0c1a4cc7ba3e 100644 (file)
@@ -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<ListUserPage> {
                     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<ListUserPage> {
         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<ListUserPage> {
     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);
+    }
   }
 }
 
index 0d73fdbcf00edc12dfcf3ae41b80de75da6d11fc..79bc43e718b9e3e1bbac82116332668eb38dc232 100644 (file)
@@ -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<ListUserPage> {
     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<EditUserPage> with MessageLine {
 #INIT_COMBO#LOAD_RECORD#ASSIGN_CONTROLLER    final formItems = <FormItem>[
 #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<EditUserPage> with MessageLine {
                           formItems,
                           screenWidth: attendedPage!.pageStates.screenWidth,
                           padding: padding,
-                    ))))));
+                        ))))));
     return rc;
   }
 
@@ -220,9 +228,7 @@ class EditUserCustom extends State<EditUserPage> 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<String, dynamic> 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<String, dynamic> map) {\n');
+      final buffer = StringBuffer('\n  void toMap(Map<String, dynamic> map) {\n');
       if (page.pageType == PageType.edit) {
         buffer.writeln("    // please set outside: map[':id'] = primaryKey;");
       }
index ecb791f118ff11d3656f78791ae08559e0aa658a..208f2fc0d178663af1440949cc672e39cf2ab089 100644 (file)
@@ -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<PageMetaData>? 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<WidgetMetaData> 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<String>(
+        '', (previous, element) => previous + indentString + element + '\n');
     return rc;
   }
 
index 616389e5b65a0f23ecf7f7978ceca205d0f435b6..1eded90f69a8c5fd0840754bdfb7b48b676a1dd2 100644 (file)
@@ -6,10 +6,10 @@ module: Roles
 list:
   type: list
   parameters: []
+  order: "role_id"
   sql: "SELECT
     t0.*
     FROM roles t0
-
     ;"
 byId:
   type: record
index 111f673cfe846cef288bb2e0314c25c97592c6c1..9595e1d8a4fb05d59e751255f4879cfd6f95c251 100644 (file)
@@ -6,10 +6,10 @@ module: Structures
 list:
   type: list
   parameters: []
+  order: "structure_id"
   sql: "SELECT
     t0.*
     FROM structures t0
-
     ;"
 byId:
   type: record
index 7050e27da20cc96ece64dee5b340f22caf7f5d4e..5102dfe611233d621185abbfed9de0f81436cf33 100644 (file)
@@ -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