From 47c37c5e744fdcd7737615e19625479c08c30427 Mon Sep 17 00:00:00 2001 From: 4831c0 <4831c0@proton.me> Date: Wed, 19 Mar 2025 19:41:35 +0100 Subject: [PATCH] 3.1.0+1 --- CHANGELOG.md | 1 + LICENSE | 202 +++++++ README.md | 1 + analysis_options.yaml | 9 + build.yaml | 8 + lib/isar_generator.dart | 11 + lib/src/code_gen/by_index_generator.dart | 119 ++++ .../code_gen/collection_schema_generator.dart | 112 ++++ .../code_gen/query_distinct_by_generator.dart | 27 + lib/src/code_gen/query_filter_generator.dart | 278 +++++++++ lib/src/code_gen/query_filter_length.dart | 50 ++ lib/src/code_gen/query_link_generator.dart | 38 ++ lib/src/code_gen/query_object_generator.dart | 29 + .../code_gen/query_property_generator.dart | 25 + lib/src/code_gen/query_sort_by_generator.dart | 55 ++ lib/src/code_gen/query_where_generator.dart | 568 ++++++++++++++++++ lib/src/code_gen/type_adapter_generator.dart | 476 +++++++++++++++ lib/src/collection_generator.dart | 105 ++++ lib/src/helper.dart | 184 ++++++ lib/src/isar_analyzer.dart | 502 ++++++++++++++++ lib/src/isar_type.dart | 107 ++++ lib/src/object_info.dart | 211 +++++++ pubspec.yaml | 26 + test/error_test.dart | 33 + test/errors/class/abstract.dart | 8 + test/errors/class/collection_supertype.dart | 19 + test/errors/class/constructor_named.dart | 10 + .../class/constructor_unknown_parameter.dart | 13 + .../class/constructor_wrong_parameter.dart | 13 + test/errors/class/enum.dart | 7 + test/errors/class/invalid_name.dart | 8 + test/errors/class/mixin.dart | 7 + test/errors/class/private.dart | 9 + test/errors/class/variable.dart | 7 + test/errors/id/duplicate.dart | 10 + test/errors/id/missing.dart | 10 + .../index/composite_double_not_last.dart | 13 + .../index/composite_non_hashed_list.dart | 13 + .../composite_string_value_not_last.dart | 13 + test/errors/index/contains_id.dart | 11 + test/errors/index/double_list_hashed.dart | 11 + test/errors/index/duplicate_name.dart | 14 + test/errors/index/duplicate_property.dart | 13 + test/errors/index/invalid_name.dart | 11 + test/errors/index/non_string_hashed.dart | 11 + .../non_string_list_hashed_elements.dart | 11 + test/errors/index/non_unique_replace.dart | 11 + test/errors/index/object_hashed.dart | 14 + test/errors/index/object_list_hashed.dart | 14 + .../errors/index/property_does_not_exist.dart | 11 + .../link/backlink_target_does_no_exist.dart | 16 + .../link/backlink_target_is_backlink.dart | 19 + .../link/backlink_target_not_a_link.dart | 18 + test/errors/link/duplicate_name.dart | 18 + test/errors/link/invalid_name.dart | 16 + test/errors/link/late.dart | 15 + test/errors/link/nullable.dart | 15 + test/errors/link/target_not_a_collection.dart | 10 + test/errors/link/type_nullable.dart | 15 + test/errors/property/duplicate_name.dart | 13 + test/errors/property/enum_bool_type.dart | 17 + test/errors/property/enum_double_type.dart | 17 + test/errors/property/enum_duplicate.dart | 21 + test/errors/property/enum_float_type.dart | 17 + test/errors/property/enum_list_type.dart | 17 + test/errors/property/enum_not_annotated.dart | 14 + test/errors/property/enum_null_value.dart | 17 + test/errors/property/enum_object_type.dart | 20 + test/errors/property/invalid_name.dart | 11 + test/errors/property/null_byte.dart | 10 + test/errors/property/null_byte_element.dart | 10 + test/errors/property/unsupported_type.dart | 10 + 72 files changed, 3805 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 build.yaml create mode 100644 lib/isar_generator.dart create mode 100644 lib/src/code_gen/by_index_generator.dart create mode 100644 lib/src/code_gen/collection_schema_generator.dart create mode 100644 lib/src/code_gen/query_distinct_by_generator.dart create mode 100644 lib/src/code_gen/query_filter_generator.dart create mode 100644 lib/src/code_gen/query_filter_length.dart create mode 100644 lib/src/code_gen/query_link_generator.dart create mode 100644 lib/src/code_gen/query_object_generator.dart create mode 100644 lib/src/code_gen/query_property_generator.dart create mode 100644 lib/src/code_gen/query_sort_by_generator.dart create mode 100644 lib/src/code_gen/query_where_generator.dart create mode 100644 lib/src/code_gen/type_adapter_generator.dart create mode 100644 lib/src/collection_generator.dart create mode 100644 lib/src/helper.dart create mode 100644 lib/src/isar_analyzer.dart create mode 100644 lib/src/isar_type.dart create mode 100644 lib/src/object_info.dart create mode 100644 pubspec.yaml create mode 100644 test/error_test.dart create mode 100644 test/errors/class/abstract.dart create mode 100644 test/errors/class/collection_supertype.dart create mode 100644 test/errors/class/constructor_named.dart create mode 100644 test/errors/class/constructor_unknown_parameter.dart create mode 100644 test/errors/class/constructor_wrong_parameter.dart create mode 100644 test/errors/class/enum.dart create mode 100644 test/errors/class/invalid_name.dart create mode 100644 test/errors/class/mixin.dart create mode 100644 test/errors/class/private.dart create mode 100644 test/errors/class/variable.dart create mode 100644 test/errors/id/duplicate.dart create mode 100644 test/errors/id/missing.dart create mode 100644 test/errors/index/composite_double_not_last.dart create mode 100644 test/errors/index/composite_non_hashed_list.dart create mode 100644 test/errors/index/composite_string_value_not_last.dart create mode 100644 test/errors/index/contains_id.dart create mode 100644 test/errors/index/double_list_hashed.dart create mode 100644 test/errors/index/duplicate_name.dart create mode 100644 test/errors/index/duplicate_property.dart create mode 100644 test/errors/index/invalid_name.dart create mode 100644 test/errors/index/non_string_hashed.dart create mode 100644 test/errors/index/non_string_list_hashed_elements.dart create mode 100644 test/errors/index/non_unique_replace.dart create mode 100644 test/errors/index/object_hashed.dart create mode 100644 test/errors/index/object_list_hashed.dart create mode 100644 test/errors/index/property_does_not_exist.dart create mode 100644 test/errors/link/backlink_target_does_no_exist.dart create mode 100644 test/errors/link/backlink_target_is_backlink.dart create mode 100644 test/errors/link/backlink_target_not_a_link.dart create mode 100644 test/errors/link/duplicate_name.dart create mode 100644 test/errors/link/invalid_name.dart create mode 100644 test/errors/link/late.dart create mode 100644 test/errors/link/nullable.dart create mode 100644 test/errors/link/target_not_a_collection.dart create mode 100644 test/errors/link/type_nullable.dart create mode 100644 test/errors/property/duplicate_name.dart create mode 100644 test/errors/property/enum_bool_type.dart create mode 100644 test/errors/property/enum_double_type.dart create mode 100644 test/errors/property/enum_duplicate.dart create mode 100644 test/errors/property/enum_float_type.dart create mode 100644 test/errors/property/enum_list_type.dart create mode 100644 test/errors/property/enum_not_annotated.dart create mode 100644 test/errors/property/enum_null_value.dart create mode 100644 test/errors/property/enum_object_type.dart create mode 100644 test/errors/property/invalid_name.dart create mode 100644 test/errors/property/null_byte.dart create mode 100644 test/errors/property/null_byte_element.dart create mode 100644 test/errors/property/unsupported_type.dart diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1634a08 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +See [Isar Changelog](https://pub.dev/packages/isar/changelog) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..404dee7 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +### Code generator for the [Isar Database](https://github.com/isar/isar) please go there for documentation. \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..97dbee4 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:very_good_analysis/analysis_options.yaml + +analyzer: + errors: + cascade_invocations: ignore + avoid_positional_boolean_parameters: ignore + parameter_assignments: ignore + public_member_api_docs: ignore + use_string_buffers: ignore \ No newline at end of file diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..c22edb4 --- /dev/null +++ b/build.yaml @@ -0,0 +1,8 @@ +builders: + isar_generator: + import: "package:isar_generator/isar_generator.dart" + builder_factories: ["getIsarGenerator"] + build_extensions: { ".dart": ["isar_generator.g.part"] } + auto_apply: dependents + build_to: cache + applies_builders: ["source_gen|combining_builder"] diff --git a/lib/isar_generator.dart b/lib/isar_generator.dart new file mode 100644 index 0000000..f195185 --- /dev/null +++ b/lib/isar_generator.dart @@ -0,0 +1,11 @@ +import 'package:build/build.dart'; +import 'package:isar_generator/src/collection_generator.dart'; +import 'package:source_gen/source_gen.dart'; + +Builder getIsarGenerator(BuilderOptions options) => SharedPartBuilder( + [ + IsarCollectionGenerator(), + IsarEmbeddedGenerator(), + ], + 'isar_generator', + ); diff --git a/lib/src/code_gen/by_index_generator.dart b/lib/src/code_gen/by_index_generator.dart new file mode 100644 index 0000000..11b9e6c --- /dev/null +++ b/lib/src/code_gen/by_index_generator.dart @@ -0,0 +1,119 @@ +import 'package:dartx/dartx.dart'; +import 'package:isar_generator/src/object_info.dart'; + +String generateByIndexExtension(ObjectInfo oi) { + final uniqueIndexes = oi.indexes.where((e) => e.unique).toList(); + if (uniqueIndexes.isEmpty) { + return ''; + } + var code = + 'extension ${oi.dartName}ByIndex on IsarCollection<${oi.dartName}> {'; + for (final index in uniqueIndexes) { + code += generateSingleByIndex(oi, index); + code += generateAllByIndex(oi, index); + if (!index.properties.first.isMultiEntry) { + code += generatePutByIndex(oi, index); + } + } + return ''' + $code + }'''; +} + +extension on ObjectIndex { + String get dartName { + return properties.map((e) => e.property.dartName.capitalize()).join(); + } +} + +String generateSingleByIndex(ObjectInfo oi, ObjectIndex index) { + final params = index.properties + .map((i) => '${i.property.dartType} ${i.property.dartName}') + .join(','); + final paramsList = index.properties.map((i) => i.property.dartName).join(','); + return ''' + Future<${oi.dartName}?> getBy${index.dartName}($params) { + return getByIndex(r'${index.name}', [$paramsList]); + } + + ${oi.dartName}? getBy${index.dartName}Sync($params) { + return getByIndexSync(r'${index.name}', [$paramsList]); + } + + Future deleteBy${index.dartName}($params) { + return deleteByIndex(r'${index.name}', [$paramsList]); + } + + bool deleteBy${index.dartName}Sync($params) { + return deleteByIndexSync(r'${index.name}', [$paramsList]); + } + '''; +} + +String generateAllByIndex(ObjectInfo oi, ObjectIndex index) { + String valsName(ObjectProperty p) => '${p.dartName}Values'; + + final props = index.properties; + final params = props + .map((ip) => 'List<${ip.property.dartType}> ${valsName(ip.property)}') + .join(','); + String createValues; + if (props.length == 1) { + final p = props.first.property; + createValues = 'final values = ${valsName(p)}.map((e) => [e]).toList();'; + } else { + final lenAssert = props + .sublist(1) + .map((i) => '${valsName(i.property)}.length == len') + .join('&&'); + createValues = ''' + final len = ${valsName(props.first.property)}.length; + assert($lenAssert, 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([${props.map((ip) => '${valsName(ip.property)}[i]').join(',')}]); + } + '''; + } + return ''' + Future> getAllBy${index.dartName}($params) { + $createValues + return getAllByIndex(r'${index.name}', values); + } + + List<${oi.dartName}?> getAllBy${index.dartName}Sync($params) { + $createValues + return getAllByIndexSync(r'${index.name}', values); + } + + Future deleteAllBy${index.dartName}($params) { + $createValues + return deleteAllByIndex(r'${index.name}', values); + } + + int deleteAllBy${index.dartName}Sync($params) { + $createValues + return deleteAllByIndexSync(r'${index.name}', values); + } + '''; +} + +String generatePutByIndex(ObjectInfo oi, ObjectIndex index) { + return ''' + Future putBy${index.dartName}(${oi.dartName} object) { + return putByIndex(r'${index.name}', object); + } + + Id putBy${index.dartName}Sync(${oi.dartName} object, {bool saveLinks = true}) { + return putByIndexSync(r'${index.name}', object, saveLinks: saveLinks); + } + + Future> putAllBy${index.dartName}(List<${oi.dartName}> objects) { + return putAllByIndex(r'${index.name}', objects); + } + + List putAllBy${index.dartName}Sync(List<${oi.dartName}> objects, {bool saveLinks = true}) { + return putAllByIndexSync(r'${index.name}', objects, saveLinks: saveLinks); + } + '''; +} diff --git a/lib/src/code_gen/collection_schema_generator.dart b/lib/src/code_gen/collection_schema_generator.dart new file mode 100644 index 0000000..61a2310 --- /dev/null +++ b/lib/src/code_gen/collection_schema_generator.dart @@ -0,0 +1,112 @@ +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; +import 'package:isar_generator/src/isar_type.dart'; + +import 'package:isar_generator/src/object_info.dart'; + +String generateSchema(ObjectInfo object) { + var code = 'const ${object.dartName.capitalize()}Schema = '; + if (!object.isEmbedded) { + code += 'CollectionSchema('; + } else { + code += 'Schema('; + } + + final properties = object.objectProperties + .mapIndexed( + (i, e) => "r'${e.isarName}': ${_generatePropertySchema(object, i)}", + ) + .join(','); + + code += ''' + name: r'${object.isarName}', + id: ${object.id}, + properties: {$properties}, + + estimateSize: ${object.estimateSizeName}, + serialize: ${object.serializeName}, + deserialize: ${object.deserializeName}, + deserializeProp: ${object.deserializePropName},'''; + + if (!object.isEmbedded) { + final indexes = object.indexes + .map((e) => "r'${e.name}': ${_generateIndexSchema(e)}") + .join(','); + final links = object.links + .map((e) => "r'${e.isarName}': ${_generateLinkSchema(object, e)}") + .join(','); + final embeddedSchemas = object.embeddedDartNames.entries + .map((e) => "r'${e.key}': ${e.value.capitalize()}Schema") + .join(','); + + code += ''' + idName: r'${object.idProperty.isarName}', + indexes: {$indexes}, + links: {$links}, + embeddedSchemas: {$embeddedSchemas}, + + getId: ${object.getIdName}, + getLinks: ${object.getLinksName}, + attach: ${object.attachName}, + version: '${Isar.version}', + '''; + } + + return '$code);'; +} + +String _generatePropertySchema(ObjectInfo object, int index) { + final property = object.objectProperties[index]; + var enumMap = ''; + if (property.isEnum) { + enumMap = 'enumMap: ${property.enumValueMapName(object)},'; + } + var target = ''; + if (property.targetIsarName != null) { + target = "target: r'${property.targetIsarName}',"; + } + return ''' + PropertySchema( + id: $index, + name: r'${property.isarName}', + type: IsarType.${property.isarType.name}, + $enumMap + $target + ) + '''; +} + +String _generateIndexSchema(ObjectIndex index) { + final properties = index.properties.map((e) { + return ''' + IndexPropertySchema( + name: r'${e.property.isarName}', + type: IndexType.${e.type.name}, + caseSensitive: ${e.caseSensitive}, + )'''; + }).join(','); + + return ''' + IndexSchema( + id: ${index.id}, + name: r'${index.name}', + unique: ${index.unique}, + replace: ${index.replace}, + properties: [$properties], + )'''; +} + +String _generateLinkSchema(ObjectInfo object, ObjectLink link) { + var linkName = ''; + if (link.isBacklink) { + linkName = "linkName: r'${link.targetLinkIsarName}',"; + } + return ''' + LinkSchema( + id: ${link.id(object.isarName)}, + name: r'${link.isarName}', + target: r'${link.targetCollectionIsarName}', + single: ${link.isSingle}, + $linkName + )'''; +} diff --git a/lib/src/code_gen/query_distinct_by_generator.dart b/lib/src/code_gen/query_distinct_by_generator.dart new file mode 100644 index 0000000..0aa61b3 --- /dev/null +++ b/lib/src/code_gen/query_distinct_by_generator.dart @@ -0,0 +1,27 @@ +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; +import 'package:isar_generator/src/isar_type.dart'; +import 'package:isar_generator/src/object_info.dart'; + +String generateDistinctBy(ObjectInfo oi) { + var code = ''' + extension ${oi.dartName}QueryWhereDistinct on QueryBuilder<${oi.dartName}, ${oi.dartName}, QDistinct> {'''; + for (final property in oi.objectProperties) { + if (property.isarType == IsarType.string) { + code += ''' + QueryBuilder<${oi.dartName}, ${oi.dartName}, QDistinct>distinctBy${property.dartName.capitalize()}({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'${property.isarName}', caseSensitive: caseSensitive); + }); + }'''; + } else if (!property.isarType.containsObject) { + code += ''' + QueryBuilder<${oi.dartName}, ${oi.dartName}, QDistinct>distinctBy${property.dartName.capitalize()}() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'${property.isarName}'); + }); + }'''; + } + } + return '$code}'; +} diff --git a/lib/src/code_gen/query_filter_generator.dart b/lib/src/code_gen/query_filter_generator.dart new file mode 100644 index 0000000..38cd34b --- /dev/null +++ b/lib/src/code_gen/query_filter_generator.dart @@ -0,0 +1,278 @@ +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; +import 'package:isar_generator/src/code_gen/query_filter_length.dart'; +import 'package:isar_generator/src/isar_type.dart'; +import 'package:isar_generator/src/object_info.dart'; + +class FilterGenerator { + FilterGenerator(this.object) : objName = object.dartName; + final ObjectInfo object; + final String objName; + + String generate() { + var code = + 'extension ${objName}QueryFilter on QueryBuilder<$objName, $objName, ' + 'QFilterCondition> {'; + for (final property in object.properties) { + if (property.nullable) { + code += generateIsNull(property); + code += generateIsNotNull(property); + } + if (property.elementNullable) { + code += generateElementIsNull(property); + code += generateElementIsNotNull(property); + } + + if (!property.isarType.containsObject) { + code += generateEqualTo(property); + + if (!property.isarType.containsBool) { + code += generateGreaterThan(property); + code += generateLessThan(property); + code += generateBetween(property); + } + } + + if (property.isarType.containsString) { + code += generateStringStartsWith(property); + code += generateStringEndsWith(property); + code += generateStringContains(property); + code += generateStringMatches(property); + code += generateStringIsEmpty(property); + code += generateStringIsNotEmpty(property); + } + + if (property.isarType.isList) { + code += generateListLength(property); + } + } + return ''' + $code + }'''; + } + + String mPrefix(ObjectProperty p, [bool listElement = true]) { + final any = listElement && p.isarType.isList ? 'Element' : ''; + return 'QueryBuilder<$objName, $objName, QAfterFilterCondition> ' + '${p.dartName.decapitalize()}$any'; + } + + String generateEqualTo(ObjectProperty p) { + final optional = [ + if (p.isarType.containsString) 'bool caseSensitive = true', + if (p.isarType.containsFloat) 'double epsilon = Query.epsilon', + ].join(','); + return ''' + ${mPrefix(p)}EqualTo(${p.nScalarDartType} value ${optional.isNotBlank ? ', {$optional,}' : ''}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'${p.isarName}', + value: value, + ${p.isarType.containsString ? 'caseSensitive: caseSensitive,' : ''} + ${p.isarType.containsFloat ? 'epsilon: epsilon,' : ''} + )); + }); + }'''; + } + + String generateGreaterThan(ObjectProperty p) { + final optional = [ + 'bool include = false', + if (p.isarType.containsString) 'bool caseSensitive = true', + if (p.isarType.containsFloat) 'double epsilon = Query.epsilon', + ].join(','); + return ''' + ${mPrefix(p)}GreaterThan(${p.nScalarDartType} value, {$optional,}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'${p.isarName}', + value: value, + ${p.isarType.containsString ? 'caseSensitive: caseSensitive,' : ''} + ${p.isarType.containsFloat ? 'epsilon: epsilon,' : ''} + )); + }); + }'''; + } + + String generateLessThan(ObjectProperty p) { + final optional = [ + 'bool include = false', + if (p.isarType.containsString) 'bool caseSensitive = true', + if (p.isarType.containsFloat) 'double epsilon = Query.epsilon', + ].join(','); + return ''' + ${mPrefix(p)}LessThan(${p.nScalarDartType} value, {$optional,}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'${p.isarName}', + value: value, + ${p.isarType.containsString ? 'caseSensitive: caseSensitive,' : ''} + ${p.isarType.containsFloat ? 'epsilon: epsilon,' : ''} + )); + }); + }'''; + } + + String generateBetween(ObjectProperty p) { + final optional = [ + 'bool includeLower = true', + 'bool includeUpper = true', + if (p.isarType.containsString) 'bool caseSensitive = true', + if (p.isarType.containsFloat) 'double epsilon = Query.epsilon', + ].join(','); + return ''' + ${mPrefix(p)}Between(${p.nScalarDartType} lower, ${p.nScalarDartType} upper, {$optional,}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'${p.isarName}', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ${p.isarType.containsString ? 'caseSensitive: caseSensitive,' : ''} + ${p.isarType.containsFloat ? 'epsilon: epsilon,' : ''} + )); + }); + }'''; + } + + String generateIsNull(ObjectProperty p) { + return ''' + ${mPrefix(p, false)}IsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'${p.isarName}', + )); + }); + }'''; + } + + String generateElementIsNull(ObjectProperty p) { + return ''' + ${mPrefix(p)}IsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.elementIsNull( + property: r'${p.isarName}', + )); + }); + }'''; + } + + String generateIsNotNull(ObjectProperty p) { + return ''' + ${mPrefix(p, false)}IsNotNull() { + return QueryBuilder.apply(this, (query) { + return query + .addFilterCondition(const FilterCondition.isNotNull( + property: r'${p.isarName}', + )); + }); + }'''; + } + + String generateElementIsNotNull(ObjectProperty p) { + return ''' + ${mPrefix(p)}IsNotNull() { + return QueryBuilder.apply(this, (query) { + return query + .addFilterCondition(const FilterCondition.elementIsNotNull( + property: r'${p.isarName}', + )); + }); + }'''; + } + + String generateStringStartsWith(ObjectProperty p) { + return ''' + ${mPrefix(p)}StartsWith(String value, {bool caseSensitive = true,}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'${p.isarName}', + value: value, + caseSensitive: caseSensitive, + )); + }); + }'''; + } + + String generateStringEndsWith(ObjectProperty p) { + return ''' + ${mPrefix(p)}EndsWith(String value, {bool caseSensitive = true,}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'${p.isarName}', + value: value, + caseSensitive: caseSensitive, + )); + }); + }'''; + } + + String generateStringContains(ObjectProperty p) { + return ''' + ${mPrefix(p)}Contains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'${p.isarName}', + value: value, + caseSensitive: caseSensitive, + )); + }); + }'''; + } + + String generateStringMatches(ObjectProperty p) { + return ''' + ${mPrefix(p)}Matches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'${p.isarName}', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + }'''; + } + + String generateStringIsEmpty(ObjectProperty p) { + return ''' + ${mPrefix(p)}IsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'${p.isarName}', + value: '', + )); + }); + }'''; + } + + String generateStringIsNotEmpty(ObjectProperty p) { + return ''' + ${mPrefix(p)}IsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'${p.isarName}', + value: '', + )); + }); + }'''; + } + + String generateListLength(ObjectProperty p) { + return generateLength(objName, p.dartName, + (lower, includeLower, upper, includeUpper) { + return ''' + QueryBuilder.apply(this, (query) { + return query.listLength( + r'${p.isarName}', + $lower, + $includeLower, + $upper, + $includeUpper, + ); + })'''; + }); + } +} diff --git a/lib/src/code_gen/query_filter_length.dart b/lib/src/code_gen/query_filter_length.dart new file mode 100644 index 0000000..0168ee7 --- /dev/null +++ b/lib/src/code_gen/query_filter_length.dart @@ -0,0 +1,50 @@ +import 'package:dartx/dartx.dart'; + +String generateLength( + String objectName, + String propertyName, + String Function( + String lower, + String includeLower, + String upper, + String includeUpper, + ) + codeGen, +) { + return ''' + QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}LengthEqualTo(int length) { + return ${codeGen('length', 'true', 'length', 'true')}; + } + + QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}IsEmpty() { + return ${codeGen('0', 'true', '0', 'true')}; + } + + QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}IsNotEmpty() { + return ${codeGen('0', 'false', '999999', 'true')}; + } + + QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}LengthLessThan( + int length, { + bool include = false, + }) { + return ${codeGen('0', 'true', 'length', 'include')}; + } + + QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}LengthGreaterThan( + int length, { + bool include = false, + }) { + return ${codeGen('length', 'include', '999999', 'true')}; + } + + QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}LengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return ${codeGen('lower', 'includeLower', 'upper', 'includeUpper')}; + } + '''; +} diff --git a/lib/src/code_gen/query_link_generator.dart b/lib/src/code_gen/query_link_generator.dart new file mode 100644 index 0000000..d068736 --- /dev/null +++ b/lib/src/code_gen/query_link_generator.dart @@ -0,0 +1,38 @@ +import 'package:dartx/dartx.dart'; +import 'package:isar_generator/src/code_gen/query_filter_length.dart'; +import 'package:isar_generator/src/object_info.dart'; + +String generateQueryLinks(ObjectInfo oi) { + var code = + 'extension ${oi.dartName}QueryLinks on QueryBuilder<${oi.dartName}, ' + '${oi.dartName}, QFilterCondition> {'; + for (final link in oi.links) { + code += ''' + QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterFilterCondition> ${link.dartName.decapitalize()}(FilterQuery<${link.targetCollectionDartName}> q) { + return QueryBuilder.apply(this, (query) { + return query.link(q, r'${link.isarName}'); + }); + }'''; + + if (link.isSingle) { + code += ''' + QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterFilterCondition> ${link.dartName.decapitalize()}IsNull() { + return QueryBuilder.apply(this, (query) { + return query.linkLength(r'${link.isarName}', 0, true, 0, true); + }); + }'''; + } else { + code += generateLength(oi.dartName, link.dartName, + (lower, includeLower, upper, includeUpper) { + return ''' + QueryBuilder.apply(this, (query) { + return query.linkLength(r'${link.isarName}', $lower, $includeLower, $upper, $includeUpper); + })'''; + }); + } + } + + return ''' + $code + }'''; +} diff --git a/lib/src/code_gen/query_object_generator.dart b/lib/src/code_gen/query_object_generator.dart new file mode 100644 index 0000000..444cb7e --- /dev/null +++ b/lib/src/code_gen/query_object_generator.dart @@ -0,0 +1,29 @@ +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; +import 'package:isar_generator/src/isar_type.dart'; +import 'package:isar_generator/src/object_info.dart'; + +String generateQueryObjects(ObjectInfo oi) { + var code = + 'extension ${oi.dartName}QueryObject on QueryBuilder<${oi.dartName}, ' + '${oi.dartName}, QFilterCondition> {'; + for (final property in oi.objectProperties) { + if (!property.isarType.containsObject) { + continue; + } + var name = property.dartName.decapitalize(); + if (property.isarType.isList) { + name += 'Element'; + } + code += ''' + QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterFilterCondition> $name(FilterQuery<${property.typeClassName}> q) { + return QueryBuilder.apply(this, (query) { + return query.object(q, r'${property.isarName}'); + }); + }'''; + } + + return ''' + $code + }'''; +} diff --git a/lib/src/code_gen/query_property_generator.dart b/lib/src/code_gen/query_property_generator.dart new file mode 100644 index 0000000..72561df --- /dev/null +++ b/lib/src/code_gen/query_property_generator.dart @@ -0,0 +1,25 @@ +import 'package:isar_generator/src/object_info.dart'; + +String generatePropertyQuery(ObjectInfo oi) { + var code = ''' + extension ${oi.dartName}QueryProperty on QueryBuilder<${oi.dartName}, ${oi.dartName}, QQueryProperty> {'''; + + // Ids are always non-nullable regardless of their specified nullability + code += ''' + QueryBuilder<${oi.dartName}, int, QQueryOperations>${oi.idProperty.dartName}Property() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'${oi.idProperty.isarName}'); + }); + }'''; + + for (final property in oi.objectProperties) { + code += ''' + QueryBuilder<${oi.dartName}, ${property.dartType}, QQueryOperations>${property.dartName}Property() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'${property.isarName}'); + }); + }'''; + } + + return '$code}'; +} diff --git a/lib/src/code_gen/query_sort_by_generator.dart b/lib/src/code_gen/query_sort_by_generator.dart new file mode 100644 index 0000000..5a8b21a --- /dev/null +++ b/lib/src/code_gen/query_sort_by_generator.dart @@ -0,0 +1,55 @@ +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; +import 'package:isar_generator/src/isar_type.dart'; + +import 'package:isar_generator/src/object_info.dart'; + +String generateSortBy(ObjectInfo oi) { + var code = ''' + extension ${oi.dartName}QuerySortBy on QueryBuilder<${oi.dartName}, ${oi.dartName}, QSortBy> {'''; + + for (final property in oi.objectProperties) { + if (property.isarType.isList || property.isarType.containsObject) { + continue; + } + + code += ''' + QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterSortBy>sortBy${property.dartName.capitalize()}() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'${property.isarName}', Sort.asc); + }); + } + + QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterSortBy>sortBy${property.dartName.capitalize()}Desc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'${property.isarName}', Sort.desc); + }); + }'''; + } + + code += ''' + } + + extension ${oi.dartName}QuerySortThenBy on QueryBuilder<${oi.dartName}, ${oi.dartName}, QSortThenBy> {'''; + + for (final property in oi.properties) { + if (property.isarType.isList || property.isarType.containsObject) { + continue; + } + + code += ''' + QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterSortBy>thenBy${property.dartName.capitalize()}() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'${property.isarName}', Sort.asc); + }); + } + + QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterSortBy>thenBy${property.dartName.capitalize()}Desc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'${property.isarName}', Sort.desc); + }); + }'''; + } + + return '$code}'; +} diff --git a/lib/src/code_gen/query_where_generator.dart b/lib/src/code_gen/query_where_generator.dart new file mode 100644 index 0000000..48dcb31 --- /dev/null +++ b/lib/src/code_gen/query_where_generator.dart @@ -0,0 +1,568 @@ +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; +import 'package:isar_generator/src/object_info.dart'; + +class WhereGenerator { + WhereGenerator(this.object) + : objName = object.dartName, + id = object.idProperty; + final ObjectInfo object; + final String objName; + final ObjectProperty id; + final existing = {}; + + String generate() { + var code = 'extension ${objName}QueryWhereSort on QueryBuilder<$objName, ' + '$objName, QWhere> {'; + + code += generateAnyId(); + for (final index in object.indexes) { + if (index.properties.all((element) => element.type == IndexType.value)) { + code += generateAny(index); + } + } + + code += ''' + } + + extension ${objName}QueryWhere on QueryBuilder<$objName, $objName, QWhereClause> { + '''; + + code += generateWhereIdEqualTo(); + code += generateWhereIdNotEqualTo(); + code += generateWhereIdGreaterThan(); + code += generateWhereIdLessThan(); + code += generateWhereIdBetween(); + + for (final index in object.indexes) { + for (var n = 0; n < index.properties.length; n++) { + final indexProperty = index.properties[n]; + final property = indexProperty.property; + + if ((property.nullable && !indexProperty.isMultiEntry) || + (property.elementNullable && indexProperty.isMultiEntry)) { + code += generateWhereIsNull(index, n + 1); + code += generateWhereIsNotNull(index, n + 1); + } + + code += generateWhereEqualTo(index, n + 1); + code += generateWhereNotEqualTo(index, n + 1); + + if (indexProperty.type == IndexType.value) { + if (property.isarType != IsarType.bool && + property.isarType != IsarType.boolList) { + code += generateWhereGreaterThan(index, n + 1); + code += generateWhereLessThan(index, n + 1); + code += generateWhereBetween(index, n + 1); + } + + if (property.isarType == IsarType.string || + property.isarType == IsarType.stringList) { + code += generateWhereStartsWith(index, n + 1); + code += generateStringIsEmpty(index, n + 1); + code += generateStringIsNotEmpty(index, n + 1); + } + } + } + } + + return '$code}'; + } + + String getMethodName(ObjectIndex index, int propertyCount, [String? method]) { + String propertyName(ObjectIndexProperty p) { + var name = p.property.dartName.capitalize(); + if (p.isMultiEntry) { + name += 'Element'; + } + return name; + } + + var name = ''; + final eqProperties = + index.properties.sublist(0, propertyCount - (method != null ? 1 : 0)); + if (eqProperties.isNotEmpty) { + name += eqProperties.map(propertyName).join(); + name += 'EqualTo'; + } + + if (method != null) { + name += propertyName(index.properties[propertyCount - 1]); + name += method; + } + + final remainingProperties = propertyCount < index.properties.length + ? index.properties.sublist(propertyCount) + : null; + + if (remainingProperties != null) { + name += 'Any'; + name += remainingProperties.map(propertyName).join(); + } + + return name.decapitalize(); + } + + String paramType(ObjectIndexProperty p) { + if (p.property.isarType.isList && p.type == IndexType.hash) { + return p.property.dartType; + } else { + return p.property.nScalarDartType; + } + } + + String paramName(ObjectIndexProperty p) { + if (p.property.isarType.isList && p.type != IndexType.hash) { + return '${p.property.dartName}Element'; + } else { + return p.property.dartName; + } + } + + String joinToParams(List properties) { + return properties + .map((it) => '${paramType(it)} ${paramName(it)}') + .join(','); + } + + String joinToValues(List properties) { + return properties.map((it) { + if (it.property.isarType.isList && it.type != IndexType.hash) { + return '${it.property.dartName}Element'; + } else { + return paramName(it); + } + }).join(', '); + } + + String generateAnyId() { + return ''' + QueryBuilder<$objName, $objName, QAfterWhere> any${id.dartName.capitalize()}() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } + '''; + } + + String generateAny(ObjectIndex index) { + final name = getMethodName(index, 0); + if (!existing.add(name)) { + return ''; + } + return ''' + QueryBuilder<$objName, $objName, QAfterWhere> $name() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + const IndexWhereClause.any(indexName: r'${index.name}'), + ); + }); + } + '''; + } + + String get mPrefix => 'QueryBuilder<$objName, $objName, QAfterWhereClause>'; + + String generateWhereIdEqualTo() { + final idName = id.dartName.decapitalize(); + return ''' + $mPrefix ${idName}EqualTo(Id $idName) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: $idName, + upper: $idName, + )); + }); + } + '''; + } + + String generateWhereEqualTo(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount); + if (!existing.add(name)) { + return ''; + } + + final properties = index.properties.takeFirst(propertyCount); + final values = joinToValues(properties); + final params = joinToParams(properties); + return ''' + $mPrefix $name($params ${properties.containsFloat ? ', {double epsilon = Query.epsilon,}' : ''}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'${index.name}', + value: [$values], + ${properties.containsFloat ? 'epsilon: epsilon,' : ''} + )); + }); + } + '''; + } + + String generateWhereIdNotEqualTo() { + final idName = id.dartName.decapitalize(); + return ''' + $mPrefix ${idName}NotEqualTo(Id $idName) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: $idName, includeUpper: false), + ).addWhereClause( + IdWhereClause.greaterThan(lower: $idName, includeLower: false), + ); + } else { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: $idName, includeLower: false), + ).addWhereClause( + IdWhereClause.lessThan(upper: $idName, includeUpper: false), + ); + } + }); + } + '''; + } + + String generateWhereNotEqualTo(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount, 'NotEqualTo'); + if (!existing.add(name)) { + return ''; + } + + final properties = index.properties.takeFirst(propertyCount); + final params = joinToParams(properties); + + final equalProperties = properties.dropLast(1); + final notEqualProperty = properties.last; + final equalValues = joinToValues(equalProperties); + var notEqualValue = joinToValues([notEqualProperty]); + if (equalValues.isNotEmpty) { + notEqualValue = ',$notEqualValue'; + } + + return ''' + $mPrefix $name($params ${properties.containsFloat ? ', {double epsilon = Query.epsilon,}' : ''}) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'${index.name}', + lower: [$equalValues], + upper: [$equalValues $notEqualValue], + includeUpper: false, + ${properties.containsFloat ? 'epsilon: epsilon,' : ''} + )).addWhereClause(IndexWhereClause.between( + indexName: r'${index.name}', + lower: [$equalValues $notEqualValue], + includeLower: false, + upper: [$equalValues], + ${properties.containsFloat ? 'epsilon: epsilon,' : ''} + )); + } else { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'${index.name}', + lower: [$equalValues $notEqualValue], + includeLower: false, + upper: [$equalValues], + ${properties.containsFloat ? 'epsilon: epsilon,' : ''} + )).addWhereClause(IndexWhereClause.between( + indexName: r'${index.name}', + lower: [$equalValues], + upper: [$equalValues $notEqualValue], + includeUpper: false, + ${properties.containsFloat ? 'epsilon: epsilon,' : ''} + )); + } + }); + } + '''; + } + + String generateWhereIdGreaterThan() { + final idName = id.dartName.decapitalize(); + return ''' + $mPrefix ${idName}GreaterThan(Id $idName, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: $idName, includeLower: include), + ); + }); + } + '''; + } + + String generateWhereGreaterThan(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount, 'GreaterThan'); + if (!existing.add(name)) { + return ''; + } + + final properties = index.properties.takeFirst(propertyCount); + final optional = [ + 'bool include = false', + if (properties.containsFloat) 'double epsilon = Query.epsilon', + ].join(','); + return ''' + $mPrefix $name(${joinToParams(properties)}, {$optional,}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'${index.name}', + lower: [${joinToValues(properties)}], + includeLower: include, + upper: [${joinToValues(properties.dropLast(1))}], + ${properties.containsFloat ? 'epsilon: epsilon,' : ''} + )); + }); + } + '''; + } + + String generateWhereIdLessThan() { + final idName = id.dartName.decapitalize(); + return ''' + $mPrefix ${idName}LessThan(Id $idName, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: $idName, includeUpper: include), + ); + }); + } + '''; + } + + String generateWhereLessThan(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount, 'LessThan'); + if (!existing.add(name)) { + return ''; + } + + final properties = index.properties.takeFirst(propertyCount); + final optional = [ + 'bool include = false', + if (properties.containsFloat) 'double epsilon = Query.epsilon', + ].join(','); + return ''' + $mPrefix $name(${joinToParams(properties)}, {$optional,}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'${index.name}', + lower: [${joinToValues(properties.dropLast(1))}], + upper: [${joinToValues(properties)}], + includeUpper: include, + ${properties.containsFloat ? 'epsilon: epsilon,' : ''} + )); + }); + } + '''; + } + + String generateWhereIdBetween() { + final idName = id.dartName.decapitalize(); + final lowerName = 'lower${id.dartName.capitalize()}'; + final upperName = 'upper${id.dartName.capitalize()}'; + return ''' + $mPrefix ${idName}Between(Id $lowerName, Id $upperName, {bool includeLower = true, bool includeUpper = true,}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: $lowerName, + includeLower: includeLower, + upper: $upperName, + includeUpper: includeUpper, + )); + }); + } + '''; + } + + String generateWhereBetween(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount, 'Between'); + if (!existing.add(name)) { + return ''; + } + + final properties = index.properties.takeFirst(propertyCount); + final equalProperties = properties.dropLast(1); + final betweenProperty = properties.last; + var params = joinToParams(equalProperties); + if (params.isNotEmpty) { + params += ','; + } + + final betweenType = paramType(betweenProperty); + final lowerName = 'lower${paramName(betweenProperty).capitalize()}'; + final upperName = 'upper${paramName(betweenProperty).capitalize()}'; + params += '$betweenType $lowerName, $betweenType $upperName'; + + var values = joinToValues(equalProperties); + if (values.isNotEmpty) { + values += ','; + } + + final optional = [ + 'bool includeLower = true', + 'bool includeUpper = true', + if (properties.containsFloat) 'double epsilon = Query.epsilon', + ].join(','); + return ''' + $mPrefix $name($params, {$optional,}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'${index.name}', + lower: [$values $lowerName], + includeLower: includeLower, + upper: [$values $upperName], + includeUpper: includeUpper, + ${properties.containsFloat ? 'epsilon: epsilon,' : ''} + )); + }); + } + '''; + } + + String generateWhereIsNull(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount, 'IsNull'); + if (!existing.add(name)) { + return ''; + } + + final properties = index.properties.takeFirst(propertyCount - 1); + var values = joinToValues(properties); + if (values.isNotEmpty) { + values += ','; + } + final params = joinToParams(properties); + return ''' + $mPrefix $name($params) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'${index.name}', + value: [$values null], + )); + }); + } + '''; + } + + String generateWhereIsNotNull(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount, 'IsNotNull'); + if (!existing.add(name)) { + return ''; + } + + final properties = index.properties.takeFirst(propertyCount - 1); + var values = joinToValues(properties); + if (values.isNotEmpty) { + values += ','; + } + final params = joinToParams(properties); + return ''' + $mPrefix $name($params) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'${index.name}', + lower: [$values null], + includeLower: false, + upper: [$values], + )); + }); + } + '''; + } + + String generateWhereStartsWith(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount, 'StartsWith'); + if (!existing.add(name)) { + return ''; + } + + final equalProperties = index.properties.dropLast(1); + var params = joinToParams(equalProperties); + if (params.isNotEmpty) { + params += ','; + } + + final prefixProperty = index.properties.last; + final prefixName = '${paramName(prefixProperty).capitalize()}Prefix'; + params += 'String $prefixName'; + var values = joinToValues(equalProperties); + if (values.isNotEmpty) { + values += ','; + } + + return ''' + $mPrefix $name($params) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'${index.name}', + lower: [$values $prefixName], + upper: [$values '\$$prefixName\\u{FFFFF}'], + )); + }); + } + '''; + } + + String generateStringIsEmpty(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount, 'IsEmpty'); + if (!existing.add(name)) { + return ''; + } + + final properties = index.properties.dropLast(1); + var values = joinToValues(properties); + if (values.isNotEmpty) { + values += ','; + } + final params = joinToParams(properties); + + return ''' + $mPrefix $name($params) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'${index.name}', + value: [$values ''], + )); + }); + }'''; + } + + String generateStringIsNotEmpty(ObjectIndex index, int propertyCount) { + final name = getMethodName(index, propertyCount, 'IsNotEmpty'); + if (!existing.add(name)) { + return ''; + } + + final properties = index.properties.dropLast(1); + var values = joinToValues(properties); + if (values.isNotEmpty) { + values += ','; + } + final params = joinToParams(properties); + + return ''' + $mPrefix $name($params) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query.addWhereClause(IndexWhereClause.lessThan( + indexName: r'${index.name}', + upper: [''], + )).addWhereClause(IndexWhereClause.greaterThan( + indexName: r'${index.name}', + lower: [''], + )); + } else { + return query.addWhereClause(IndexWhereClause.greaterThan( + indexName: r'${index.name}', + lower: [''], + )).addWhereClause(IndexWhereClause.lessThan( + indexName: r'${index.name}', + upper: [''], + )); + } + }); + }'''; + } +} + +extension on List { + bool get containsFloat => + last.isarType == IsarType.float || last.isarType == IsarType.floatList; +} diff --git a/lib/src/code_gen/type_adapter_generator.dart b/lib/src/code_gen/type_adapter_generator.dart new file mode 100644 index 0000000..16f7d56 --- /dev/null +++ b/lib/src/code_gen/type_adapter_generator.dart @@ -0,0 +1,476 @@ +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; +import 'package:isar_generator/src/object_info.dart'; + +String _prepareSerialize( + bool nullable, + String value, + String Function(String) size, +) { + var code = ''; + if (nullable) { + code += ''' + { + final value = $value; + if (value != null) {'''; + value = 'value'; + } + code += 'bytesCount += ${size(value)};'; + if (nullable) { + code += '}}'; + } + return code; +} + +String _prepareSerializeList( + bool nullable, + bool elementNullable, + String value, + String size, [ + String? prepare, +]) { + var code = ''; + if (nullable) { + code += ''' + { + final list = $value; + if (list != null) {'''; + value = 'list'; + } + code += ''' + bytesCount += 3 + $value.length * 3; + { + ${prepare ?? ''} + for (var i = 0; i < $value.length; i++) { + final value = $value[i];'''; + if (elementNullable) { + code += 'if (value != null) {'; + } + code += 'bytesCount += $size;'; + if (elementNullable) { + code += '}'; + } + code += '}}'; + if (nullable) { + code += '}}'; + } + return code; +} + +String generateEstimateSerialize(ObjectInfo object) { + var code = ''' + int ${object.estimateSizeName}( + ${object.dartName} object, + List offsets, + Map> allOffsets, + ) { + var bytesCount = offsets.last;'''; + + for (final property in object.properties) { + final value = 'object.${property.dartName}'; + + switch (property.isarType) { + case IsarType.string: + final enumValue = property.isEnum ? '.${property.enumProperty}' : ''; + code += _prepareSerialize( + property.nullable, + value, + (value) => '3 + $value$enumValue.length * 3', + ); + break; + + case IsarType.stringList: + final enumValue = property.isEnum ? '.${property.enumProperty}' : ''; + code += _prepareSerializeList( + property.nullable, + property.elementNullable, + value, + 'value$enumValue.length * 3', + ); + break; + + case IsarType.object: + code += _prepareSerialize( + property.nullable, + value, + (value) { + return '3 + ${property.targetSchema}.estimateSize($value, ' + 'allOffsets[${property.scalarDartType}]!, allOffsets)'; + }, + ); + break; + + case IsarType.objectList: + code += _prepareSerializeList( + property.nullable, + property.elementNullable, + value, + '${property.targetSchema}.estimateSize(value, offsets, allOffsets)', + 'final offsets = allOffsets[${property.scalarDartType}]!;', + ); + break; + + case IsarType.byteList: + case IsarType.boolList: + code += _prepareSerialize( + property.nullable, + value, + (value) => '3 + $value.length', + ); + break; + case IsarType.intList: + case IsarType.floatList: + code += _prepareSerialize( + property.nullable, + value, + (value) => '3 + $value.length * 4', + ); + break; + case IsarType.longList: + case IsarType.doubleList: + case IsarType.dateTimeList: + code += _prepareSerialize( + property.nullable, + value, + (value) => '3 + $value.length * 8', + ); + break; + + // ignore: no_default_cases + default: + break; + } + } + + return ''' + $code + return bytesCount; + }'''; +} + +String generateSerialize(ObjectInfo object) { + var code = ''' + void ${object.serializeName}( + ${object.dartName} object, + IsarWriter writer, + List offsets, + Map> allOffsets, + ) {'''; + + for (var i = 0; i < object.objectProperties.length; i++) { + final property = object.objectProperties[i]; + var value = 'object.${property.dartName}'; + if (property.isEnum) { + final nOp = property.nullable ? '?' : ''; + final elNOp = property.elementNullable ? '?' : ''; + value = property.isarType.isList + ? '$value$nOp.map((e) => e$elNOp.${property.enumProperty}).toList()' + : '$value$nOp.${property.enumProperty}'; + } + + switch (property.isarType) { + case IsarType.bool: + code += 'writer.writeBool(offsets[$i], $value);'; + break; + case IsarType.byte: + code += 'writer.writeByte(offsets[$i], $value);'; + break; + case IsarType.int: + code += 'writer.writeInt(offsets[$i], $value);'; + break; + case IsarType.float: + code += 'writer.writeFloat(offsets[$i], $value);'; + break; + case IsarType.long: + code += 'writer.writeLong(offsets[$i], $value);'; + break; + case IsarType.double: + code += 'writer.writeDouble(offsets[$i], $value);'; + break; + case IsarType.dateTime: + code += 'writer.writeDateTime(offsets[$i], $value);'; + break; + case IsarType.string: + code += 'writer.writeString(offsets[$i], $value);'; + break; + case IsarType.object: + code += ''' + writer.writeObject<${property.typeClassName}>( + offsets[$i], + allOffsets, + ${property.targetSchema}.serialize, + $value, + );'''; + break; + case IsarType.byteList: + code += 'writer.writeByteList(offsets[$i], $value);'; + break; + case IsarType.boolList: + code += 'writer.writeBoolList(offsets[$i], $value);'; + break; + case IsarType.intList: + code += 'writer.writeIntList(offsets[$i], $value);'; + break; + case IsarType.longList: + code += 'writer.writeLongList(offsets[$i], $value);'; + break; + case IsarType.floatList: + code += 'writer.writeFloatList(offsets[$i], $value);'; + break; + case IsarType.doubleList: + code += 'writer.writeDoubleList(offsets[$i], $value);'; + break; + case IsarType.dateTimeList: + code += 'writer.writeDateTimeList(offsets[$i], $value);'; + break; + case IsarType.stringList: + code += 'writer.writeStringList(offsets[$i], $value);'; + break; + case IsarType.objectList: + code += ''' + writer.writeObjectList<${property.typeClassName}>( + offsets[$i], + allOffsets, + ${property.targetSchema}.serialize, + $value, + );'''; + break; + } + } + + return '$code}'; +} + +String generateDeserialize(ObjectInfo object) { + var code = ''' + ${object.dartName} ${object.deserializeName}( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, + ) { + final object = ${object.dartName}('''; + + final propertiesByMode = + object.properties.groupBy((ObjectProperty p) => p.deserialize); + final positional = propertiesByMode[PropertyDeser.positionalParam] ?? []; + final sortedPositional = + positional.sortedBy((ObjectProperty p) => p.constructorPosition!); + for (final p in sortedPositional) { + final index = object.objectProperties.indexOf(p); + final deser = _deserializeProperty(object, p, 'offsets[$index]'); + code += '$deser,'; + } + + final named = propertiesByMode[PropertyDeser.namedParam] ?? []; + for (final p in named) { + final index = object.objectProperties.indexOf(p); + final deser = _deserializeProperty(object, p, 'offsets[$index]'); + code += '${p.dartName}: $deser,'; + } + + code += ');'; + + final assign = propertiesByMode[PropertyDeser.assign] ?? []; + for (final p in assign) { + final index = object.objectProperties.indexOf(p); + final deser = _deserializeProperty(object, p, 'offsets[$index]'); + code += 'object.${p.dartName} = $deser;'; + } + + return ''' + $code + return object; + }'''; +} + +String generateDeserializeProp(ObjectInfo object) { + var code = ''' + P ${object.deserializePropName}

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, + ) { + switch (propertyId) {'''; + + for (var i = 0; i < object.objectProperties.length; i++) { + final property = object.objectProperties[i]; + final deser = _deserializeProperty(object, property, 'offset'); + code += 'case $i: return ($deser) as P;'; + } + + return ''' + $code + default: + throw IsarError('Unknown property with id \$propertyId'); + } + } + '''; +} + +String _deserializeProperty( + ObjectInfo object, + ObjectProperty property, + String propertyOffset, +) { + if (property.isId) { + return 'id'; + } + + final deser = _deserialize(property, propertyOffset); + + var defaultValue = ''; + if (!property.nullable) { + if (property.userDefaultValue != null) { + defaultValue = '?? ${property.userDefaultValue}'; + } else if (property.isarType == IsarType.object) { + defaultValue = '?? ${property.typeClassName}()'; + } else if (property.isarType.isList) { + defaultValue = '?? []'; + } else if (property.isEnum) { + defaultValue = '?? ${property.defaultEnumElement}'; + } + } + + if (property.isEnum) { + if (property.isarType.isList) { + final elDefault = + !property.elementNullable ? '?? ${property.defaultEnumElement}' : ''; + return '$deser?.map((e) => ${property.valueEnumMapName(object)}[e] ' + '$elDefault).toList() $defaultValue'; + } else { + return '${property.valueEnumMapName(object)}[$deser] $defaultValue'; + } + } else { + return '$deser $defaultValue'; + } +} + +String _deserialize(ObjectProperty property, String propertyOffset) { + final orNull = + property.nullable || property.userDefaultValue != null || property.isEnum + ? 'OrNull' + : ''; + final orElNull = property.elementNullable ? 'OrNull' : ''; + + switch (property.isarType) { + case IsarType.bool: + return 'reader.readBool$orNull($propertyOffset)'; + case IsarType.byte: + return 'reader.readByte$orNull($propertyOffset)'; + case IsarType.int: + return 'reader.readInt$orNull($propertyOffset)'; + case IsarType.float: + return 'reader.readFloat$orNull($propertyOffset)'; + case IsarType.long: + return 'reader.readLong$orNull($propertyOffset)'; + case IsarType.double: + return 'reader.readDouble$orNull($propertyOffset)'; + case IsarType.dateTime: + return 'reader.readDateTime$orNull($propertyOffset)'; + case IsarType.string: + return 'reader.readString$orNull($propertyOffset)'; + case IsarType.object: + return ''' + reader.readObjectOrNull<${property.typeClassName}>( + $propertyOffset, + ${property.targetSchema}.deserialize, + allOffsets, + )'''; + case IsarType.boolList: + return 'reader.readBool${orElNull}List($propertyOffset)'; + case IsarType.byteList: + return 'reader.readByteList($propertyOffset)'; + case IsarType.intList: + return 'reader.readInt${orElNull}List($propertyOffset)'; + case IsarType.floatList: + return 'reader.readFloat${orElNull}List($propertyOffset)'; + case IsarType.longList: + return 'reader.readLong${orElNull}List($propertyOffset)'; + case IsarType.doubleList: + return 'reader.readDouble${orElNull}List($propertyOffset)'; + case IsarType.dateTimeList: + return 'reader.readDateTime${orElNull}List($propertyOffset)'; + case IsarType.stringList: + return 'reader.readString${orElNull}List($propertyOffset)'; + case IsarType.objectList: + return ''' + reader.readObject${orElNull}List<${property.typeClassName}>( + $propertyOffset, + ${property.targetSchema}.deserialize, + allOffsets, + ${!property.elementNullable ? '${property.typeClassName}(),' : ''} + )'''; + } +} + +String generateGetId(ObjectInfo object) { + final defaultVal = object.idProperty.nullable ? '?? Isar.autoIncrement' : ''; + return ''' + Id ${object.getIdName}(${object.dartName} object) { + return object.${object.idProperty.dartName} $defaultVal; + } + '''; +} + +String generateGetLinks(ObjectInfo object) { + return ''' + List> ${object.getLinksName}(${object.dartName} object) { + return [${object.links.map((e) => 'object.${e.dartName}').join(',')}]; + } + '''; +} + +String generateAttach(ObjectInfo object) { + var code = ''' + void ${object.attachName}(IsarCollection col, Id id, ${object.dartName} object) {'''; + + if (object.idProperty.assignable) { + code += 'object.${object.idProperty.dartName} = id;'; + } + + for (final link in object.links) { + // ignore: leading_newlines_in_multiline_strings + code += '''object.${link.dartName}.attach( + col, + col.isar.collection<${link.targetCollectionDartName}>(), + r'${link.isarName}', + id + );'''; + } + return '$code}'; +} + +String generateEnumMaps(ObjectInfo object) { + var code = ''; + for (final property in object.properties) { + final enumName = property.typeClassName; + if (property.isEnum) { + code += 'const ${property.enumValueMapName(object)} = {'; + for (final enumElementName in property.enumMap!.keys) { + final value = property.enumMap![enumElementName]; + if (value is String) { + code += "r'$enumElementName': r'$value',"; + } else { + code += "'$enumElementName': $value,"; + } + } + code += '};'; + + code += 'const ${property.valueEnumMapName(object)} = {'; + for (final enumElementName in property.enumMap!.keys) { + final value = property.enumMap![enumElementName]; + if (value is String) { + code += "r'$value': $enumName.$enumElementName,"; + } else { + code += '$value: $enumName.$enumElementName,'; + } + } + code += '};'; + } + } + + return code; +} diff --git a/lib/src/collection_generator.dart b/lib/src/collection_generator.dart new file mode 100644 index 0000000..9231281 --- /dev/null +++ b/lib/src/collection_generator.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart'; +import 'package:isar/isar.dart'; +import 'package:isar_generator/src/code_gen/by_index_generator.dart'; +import 'package:isar_generator/src/code_gen/collection_schema_generator.dart'; +import 'package:isar_generator/src/code_gen/query_distinct_by_generator.dart'; +import 'package:isar_generator/src/code_gen/query_filter_generator.dart'; +import 'package:isar_generator/src/code_gen/query_link_generator.dart'; +import 'package:isar_generator/src/code_gen/query_object_generator.dart'; +import 'package:isar_generator/src/code_gen/query_property_generator.dart'; +import 'package:isar_generator/src/code_gen/query_sort_by_generator.dart'; +import 'package:isar_generator/src/code_gen/query_where_generator.dart'; +import 'package:isar_generator/src/code_gen/type_adapter_generator.dart'; +import 'package:isar_generator/src/isar_analyzer.dart'; +import 'package:source_gen/source_gen.dart'; + +const ignoreLints = [ + 'duplicate_ignore', + 'non_constant_identifier_names', + 'constant_identifier_names', + 'invalid_use_of_protected_member', + 'unnecessary_cast', + 'prefer_const_constructors', + 'lines_longer_than_80_chars', + 'require_trailing_commas', + 'inference_failure_on_function_invocation', + 'unnecessary_parenthesis', + 'unnecessary_raw_strings', + 'unnecessary_null_checks', + 'join_return_with_assignment', + 'prefer_final_locals', + 'avoid_js_rounded_ints', + 'avoid_positional_boolean_parameters', + 'always_specify_types', +]; + +class IsarCollectionGenerator extends GeneratorForAnnotation { + @override + Future generateForAnnotatedElement( + Element element, + ConstantReader annotation, + BuildStep buildStep, + ) async { + final object = IsarAnalyzer().analyzeCollection(element); + return ''' + // coverage:ignore-file + // ignore_for_file: ${ignoreLints.join(', ')} + + extension Get${object.dartName}Collection on Isar { + IsarCollection<${object.dartName}> get ${object.accessor} => this.collection(); + } + + ${generateSchema(object)} + + ${generateEstimateSerialize(object)} + ${generateSerialize(object)} + ${generateDeserialize(object)} + ${generateDeserializeProp(object)} + + ${generateEnumMaps(object)} + + ${generateGetId(object)} + ${generateGetLinks(object)} + ${generateAttach(object)} + + ${generateByIndexExtension(object)} + ${WhereGenerator(object).generate()} + ${FilterGenerator(object).generate()} + ${generateQueryObjects(object)} + ${generateQueryLinks(object)} + ${generateSortBy(object)} + ${generateDistinctBy(object)} + ${generatePropertyQuery(object)} + '''; + } +} + +class IsarEmbeddedGenerator extends GeneratorForAnnotation { + @override + Future generateForAnnotatedElement( + Element element, + ConstantReader annotation, + BuildStep buildStep, + ) async { + final object = IsarAnalyzer().analyzeEmbedded(element); + return ''' + // coverage:ignore-file + // ignore_for_file: ${ignoreLints.join(', ')} + + ${generateSchema(object)} + + ${generateEstimateSerialize(object)} + ${generateSerialize(object)} + ${generateDeserialize(object)} + ${generateDeserializeProp(object)} + + ${generateEnumMaps(object)} + + ${FilterGenerator(object).generate()} + ${generateQueryObjects(object)} + '''; + } +} diff --git a/lib/src/helper.dart b/lib/src/helper.dart new file mode 100644 index 0000000..f534b34 --- /dev/null +++ b/lib/src/helper.dart @@ -0,0 +1,184 @@ +import 'package:analyzer/dart/constant/value.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; +import 'package:source_gen/source_gen.dart'; + +const TypeChecker _collectionChecker = TypeChecker.fromRuntime(Collection); +const TypeChecker _enumeratedChecker = TypeChecker.fromRuntime(Enumerated); +const TypeChecker _embeddedChecker = TypeChecker.fromRuntime(Embedded); +const TypeChecker _ignoreChecker = TypeChecker.fromRuntime(Ignore); +const TypeChecker _nameChecker = TypeChecker.fromRuntime(Name); +const TypeChecker _indexChecker = TypeChecker.fromRuntime(Index); +const TypeChecker _backlinkChecker = TypeChecker.fromRuntime(Backlink); + +extension ClassElementX on ClassElement { + bool get hasZeroArgsConstructor { + return constructors.any( + (ConstructorElement c) => + c.isPublic && + !c.parameters.any((ParameterElement p) => !p.isOptional), + ); + } + + List get allAccessors { + final ignoreFields = + collectionAnnotation?.ignore ?? embeddedAnnotation!.ignore; + return [ + ...accessors.mapNotNull((e) => e.variable), + if (collectionAnnotation?.inheritance ?? embeddedAnnotation!.inheritance) + for (InterfaceType supertype in allSupertypes) ...[ + if (!supertype.isDartCoreObject) + ...supertype.accessors.mapNotNull((e) => e.variable) + ] + ] + .where( + (PropertyInducingElement e) => + e.isPublic && + !e.isStatic && + !_ignoreChecker.hasAnnotationOf(e.nonSynthetic) && + !ignoreFields.contains(e.name), + ) + .distinctBy((e) => e.name) + .toList(); + } + + List get enumConsts { + return fields.where((e) => e.isEnumConstant).map((e) => e.name).toList(); + } +} + +extension PropertyElementX on PropertyInducingElement { + bool get isLink => type.element2!.name == 'IsarLink'; + + bool get isLinks => type.element2!.name == 'IsarLinks'; + + Enumerated? get enumeratedAnnotation { + final ann = _enumeratedChecker.firstAnnotationOfExact(nonSynthetic); + if (ann == null) { + return null; + } + final typeIndex = ann.getField('type')!.getField('index')!.toIntValue()!; + return Enumerated( + EnumType.values[typeIndex], + ann.getField('property')?.toStringValue(), + ); + } + + Backlink? get backlinkAnnotation { + final ann = _backlinkChecker.firstAnnotationOfExact(nonSynthetic); + if (ann == null) { + return null; + } + return Backlink(to: ann.getField('to')!.toStringValue()!); + } + + List get indexAnnotations { + return _indexChecker.annotationsOfExact(nonSynthetic).map((DartObject ann) { + final rawComposite = ann.getField('composite')!.toListValue(); + final composite = []; + if (rawComposite != null) { + for (final c in rawComposite) { + final indexTypeField = c.getField('type')!; + IndexType? indexType; + if (!indexTypeField.isNull) { + final indexTypeIndex = + indexTypeField.getField('index')!.toIntValue()!; + indexType = IndexType.values[indexTypeIndex]; + } + composite.add( + CompositeIndex( + c.getField('property')!.toStringValue()!, + type: indexType, + caseSensitive: c.getField('caseSensitive')!.toBoolValue(), + ), + ); + } + } + final indexTypeField = ann.getField('type')!; + IndexType? indexType; + if (!indexTypeField.isNull) { + final indexTypeIndex = indexTypeField.getField('index')!.toIntValue()!; + indexType = IndexType.values[indexTypeIndex]; + } + return Index( + name: ann.getField('name')!.toStringValue(), + composite: composite, + unique: ann.getField('unique')!.toBoolValue()!, + replace: ann.getField('replace')!.toBoolValue()!, + type: indexType, + caseSensitive: ann.getField('caseSensitive')!.toBoolValue(), + ); + }).toList(); + } +} + +extension ElementX on Element { + String get isarName { + final ann = _nameChecker.firstAnnotationOfExact(nonSynthetic); + late String name; + if (ann == null) { + name = displayName; + } else { + name = ann.getField('name')!.toStringValue()!; + } + checkIsarName(name, this); + return name; + } + + Collection? get collectionAnnotation { + final ann = _collectionChecker.firstAnnotationOfExact(nonSynthetic); + if (ann == null) { + return null; + } + return Collection( + inheritance: ann.getField('inheritance')!.toBoolValue()!, + accessor: ann.getField('accessor')!.toStringValue(), + ignore: ann + .getField('ignore')! + .toSetValue()! + .map((e) => e.toStringValue()!) + .toSet(), + ); + } + + String get collectionAccessor { + var accessor = collectionAnnotation?.accessor; + if (accessor != null) { + return accessor; + } + + accessor = displayName.decapitalize(); + if (!accessor.endsWith('s')) { + accessor += 's'; + } + + return accessor; + } + + Embedded? get embeddedAnnotation { + final ann = _embeddedChecker.firstAnnotationOfExact(nonSynthetic); + if (ann == null) { + return null; + } + return Embedded( + inheritance: ann.getField('inheritance')!.toBoolValue()!, + ignore: ann + .getField('ignore')! + .toSetValue()! + .map((e) => e.toStringValue()!) + .toSet(), + ); + } +} + +void checkIsarName(String name, Element element) { + if (name.isBlank || name.startsWith('_')) { + err('Names must not be blank or start with "_".', element); + } +} + +Never err(String msg, [Element? element]) { + throw InvalidGenerationSourceError(msg, element: element); +} diff --git a/lib/src/isar_analyzer.dart b/lib/src/isar_analyzer.dart new file mode 100644 index 0000000..515fcf1 --- /dev/null +++ b/lib/src/isar_analyzer.dart @@ -0,0 +1,502 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; + +import 'package:isar_generator/src/helper.dart'; +import 'package:isar_generator/src/isar_type.dart'; +import 'package:isar_generator/src/object_info.dart'; + +class IsarAnalyzer { + ObjectInfo analyzeCollection(Element element) { + final constructor = _checkValidClass(element); + final modelClass = element as ClassElement; + + final properties = []; + final links = []; + for (final propertyElement in modelClass.allAccessors) { + if (propertyElement.isLink || propertyElement.isLinks) { + final link = analyzeObjectLink(propertyElement); + links.add(link); + } else { + final property = analyzeObjectProperty(propertyElement, constructor); + properties.add(property); + } + } + _checkValidPropertiesConstructor(properties, constructor); + if (links.map((e) => e.isarName).distinct().length != links.length) { + err('Two or more links have the same name.', modelClass); + } + + final indexes = []; + for (final propertyElement in modelClass.allAccessors) { + indexes.addAll(analyzeObjectIndex(properties, propertyElement)); + } + if (indexes.map((e) => e.name).distinct().length != indexes.length) { + err('Two or more indexes have the same name.', modelClass); + } + + final idProperties = properties.where((it) => it.isId); + if (idProperties.isEmpty) { + err( + 'No id property defined. Use the "Id" type for your id property.', + modelClass, + ); + } else if (idProperties.length > 1) { + err('Two or more properties with type "Id" defined.', modelClass); + } + + return ObjectInfo( + dartName: modelClass.displayName, + isarName: modelClass.isarName, + accessor: modelClass.collectionAccessor, + properties: properties, + embeddedDartNames: _getEmbeddedDartNames(element), + indexes: indexes, + links: links, + ); + } + + ObjectInfo analyzeEmbedded(Element element) { + final constructor = _checkValidClass(element); + final modelClass = element as ClassElement; + + if (constructor.parameters.any((e) => e.isRequired)) { + err( + 'Constructors of embedded objects must not have required parameters.', + constructor, + ); + } + + final properties = []; + for (final propertyElement in modelClass.allAccessors) { + if (propertyElement.isLink || propertyElement.isLinks) { + err('Embedded objects must not contain links', propertyElement); + } else { + final property = analyzeObjectProperty(propertyElement, constructor); + properties.add(property); + } + } + _checkValidPropertiesConstructor(properties, constructor); + + final hasIndex = modelClass.allAccessors.any( + (it) => it.indexAnnotations.isNotEmpty, + ); + if (hasIndex) { + err('Embedded objects must not have indexes.', modelClass); + } + + final hasIdProperty = properties.any((it) => it.isId); + if (hasIdProperty) { + err('Embedded objects must not define an id.', modelClass); + } + + return ObjectInfo( + dartName: modelClass.displayName, + isarName: modelClass.isarName, + properties: properties, + ); + } + + ConstructorElement _checkValidClass(Element modelClass) { + if (modelClass is! ClassElement || + modelClass is EnumElement || + modelClass is MixinElement) { + err( + 'Only classes may be annotated with @Collection or @Embedded.', + modelClass, + ); + } + + if (modelClass.isAbstract) { + err('Class must not be abstract.', modelClass); + } + + if (!modelClass.isPublic) { + err('Class must be public.', modelClass); + } + + final constructor = modelClass.constructors + .firstOrNullWhere((ConstructorElement c) => c.periodOffset == null); + if (constructor == null) { + err('Class needs an unnamed constructor.', modelClass); + } + + final hasCollectionSupertype = modelClass.allSupertypes.any((type) { + return type.element.collectionAnnotation != null || + type.element.embeddedAnnotation != null; + }); + if (hasCollectionSupertype) { + err( + 'Class must not have a supertype annotated with @Collection or ' + '@Embedded.', + modelClass, + ); + } + + return constructor; + } + + void _checkValidPropertiesConstructor( + List properties, + ConstructorElement constructor, + ) { + if (properties.map((e) => e.isarName).distinct().length != + properties.length) { + err( + 'Two or more properties have the same name.', + constructor.enclosingElement, + ); + } + + final unknownConstructorParameter = constructor.parameters.firstOrNullWhere( + (p) => p.isRequired && properties.none((e) => e.dartName == p.name), + ); + if (unknownConstructorParameter != null) { + err( + 'Constructor parameter does not match a property.', + unknownConstructorParameter, + ); + } + } + + Map _getEmbeddedDartNames(ClassElement element) { + void _fillNames(Map names, ClassElement element) { + for (final property in element.allAccessors) { + final type = property.type.scalarType.element; + if (type is ClassElement && type.embeddedAnnotation != null) { + final isarName = type.isarName; + if (!names.containsKey(isarName)) { + names[type.isarName] = type.displayName; + _fillNames(names, type); + } + } + } + } + + final names = {}; + _fillNames(names, element); + return names; + } + + ObjectProperty analyzeObjectProperty( + PropertyInducingElement property, + ConstructorElement constructor, + ) { + final dartType = property.type; + final scalarDartType = dartType.scalarType; + Map? enumMap; + String? enumPropertyName; + String? defaultEnumElement; + + late final IsarType isarType; + if (scalarDartType.element is EnumElement) { + final enumeratedAnn = property.enumeratedAnnotation; + if (enumeratedAnn == null) { + err('Enum property must be annotated with @enumerated.', property); + } + + final enumClass = scalarDartType.element! as EnumElement; + final enumElements = + enumClass.fields.where((f) => f.isEnumConstant).toList(); + defaultEnumElement = '${enumClass.name}.${enumElements.first.name}'; + + if (enumeratedAnn.type == EnumType.ordinal) { + isarType = dartType.isDartCoreList ? IsarType.byteList : IsarType.byte; + enumMap = { + for (var i = 0; i < enumElements.length; i++) enumElements[i].name: i, + }; + enumPropertyName = 'index'; + } else if (enumeratedAnn.type == EnumType.ordinal32) { + isarType = dartType.isDartCoreList ? IsarType.intList : IsarType.int; + + enumMap = { + for (var i = 0; i < enumElements.length; i++) enumElements[i].name: i, + }; + enumPropertyName = 'index'; + } else if (enumeratedAnn.type == EnumType.name) { + isarType = + dartType.isDartCoreList ? IsarType.stringList : IsarType.string; + enumMap = { + for (final value in enumElements) value.name: value.name, + }; + enumPropertyName = 'name'; + } else { + enumPropertyName = enumeratedAnn.property; + if (enumPropertyName == null) { + err( + 'Enums with type EnumType.value must specify which property ' + 'should be used.', + property, + ); + } + final enumProperty = enumClass.getField(enumPropertyName); + if (enumProperty == null || enumProperty.isEnumConstant) { + err('Enum property "$enumProperty" does not exist.', property); + } else if (enumProperty.nonSynthetic is PropertyAccessorElement) { + err('Only fields are supported for enum properties', enumProperty); + } + + final enumIsarType = enumProperty.type.isarType; + if (enumIsarType != IsarType.byte && + enumIsarType != IsarType.int && + enumIsarType != IsarType.long && + enumIsarType != IsarType.string) { + err('Unsupported enum property type.', enumProperty); + } + + isarType = + dartType.isDartCoreList ? enumIsarType!.listType : enumIsarType!; + enumMap = {}; + for (final element in enumElements) { + final property = + element.computeConstantValue()!.getField(enumPropertyName)!; + final propertyValue = property.toBoolValue() ?? + property.toIntValue() ?? + property.toDoubleValue() ?? + property.toStringValue(); + if (propertyValue == null) { + err( + 'Null values are not supported for enum properties.', + enumProperty, + ); + } + + if (enumMap.values.contains(propertyValue)) { + err( + 'Enum property has duplicate values.', + enumProperty, + ); + } + enumMap[element.name] = propertyValue; + } + } + } else { + if (dartType.isarType != null) { + isarType = dartType.isarType!; + } else { + err( + 'Unsupported type. Please annotate the property with @ignore.', + property, + ); + } + } + + final nullable = dartType.nullabilitySuffix != NullabilitySuffix.none; + final elementNullable = isarType.isList && + dartType.scalarType.nullabilitySuffix != NullabilitySuffix.none; + + if ((isarType == IsarType.byte && nullable) || + (isarType == IsarType.byteList && elementNullable)) { + err('Bytes must not be nullable.', property); + } + + final constructorParameter = + constructor.parameters.firstOrNullWhere((p) => p.name == property.name); + int? constructorPosition; + late PropertyDeser deserialize; + if (constructorParameter != null) { + if (constructorParameter.type != property.type) { + err( + 'Constructor parameter type does not match property type', + constructorParameter, + ); + } + deserialize = constructorParameter.isNamed + ? PropertyDeser.namedParam + : PropertyDeser.positionalParam; + constructorPosition = + constructor.parameters.indexOf(constructorParameter); + } else { + deserialize = + property.setter == null ? PropertyDeser.none : PropertyDeser.assign; + } + + return ObjectProperty( + dartName: property.displayName, + isarName: property.isarName, + typeClassName: dartType.scalarType.element!.name!, + targetIsarName: isarType.containsObject + ? dartType.scalarType.element!.isarName + : null, + isarType: isarType, + isId: dartType.isIsarId, + enumMap: enumMap, + enumProperty: enumPropertyName, + defaultEnumElement: defaultEnumElement, + nullable: nullable, + elementNullable: elementNullable, + userDefaultValue: constructorParameter?.defaultValueCode, + deserialize: deserialize, + assignable: property.setter != null, + constructorPosition: constructorPosition, + ); + } + + ObjectLink analyzeObjectLink(PropertyInducingElement property) { + if (property.type.nullabilitySuffix != NullabilitySuffix.none) { + err('Link properties must not be nullable.', property); + } else if (property.isLate) { + err('Link properties must not be late.', property); + } + + final type = property.type as ParameterizedType; + final linkType = type.typeArguments[0]; + if (linkType.nullabilitySuffix != NullabilitySuffix.none) { + err('Links type must not be nullable.', property); + } + + final targetCol = linkType.element! as ClassElement; + if (targetCol.collectionAnnotation == null) { + err('Link target is not annotated with @collection'); + } + + final backlinkAnn = property.backlinkAnnotation; + String? targetLinkIsarName; + if (backlinkAnn != null) { + final targetProperty = targetCol.allAccessors + .firstOrNullWhere((e) => e.displayName == backlinkAnn.to); + if (targetProperty == null) { + err('Target of Backlink does not exist', property); + } else if (targetProperty.backlinkAnnotation != null) { + err('Target of Backlink is also a backlink', property); + } + + if (!targetProperty.isLink && !targetProperty.isLinks) { + err('Target of backlink is not a link', property); + } + + final targetLink = analyzeObjectLink(targetProperty); + targetLinkIsarName = targetLink.isarName; + } + + return ObjectLink( + dartName: property.displayName, + isarName: property.isarName, + targetLinkIsarName: targetLinkIsarName, + targetCollectionDartName: linkType.element!.name!, + targetCollectionIsarName: targetCol.isarName, + isSingle: property.isLink, + ); + } + + Iterable analyzeObjectIndex( + List properties, + PropertyInducingElement element, + ) sync* { + final property = + properties.firstOrNullWhere((it) => it.dartName == element.name); + if (property == null || property.isId) { + return; + } + + for (final index in element.indexAnnotations) { + final indexProperties = []; + final isString = property.isarType == IsarType.string || + property.isarType == IsarType.stringList; + final defaultType = property.isarType.isList || isString + ? IndexType.hash + : IndexType.value; + + indexProperties.add( + ObjectIndexProperty( + property: property, + type: index.type ?? defaultType, + caseSensitive: index.caseSensitive ?? isString, + ), + ); + for (final c in index.composite) { + final compositeProperty = + properties.firstOrNullWhere((it) => it.dartName == c.property); + if (compositeProperty == null) { + err('Property does not exist: "${c.property}".', element); + } else if (compositeProperty.isId) { + err('Ids cannot be indexed', element); + } else { + final isString = compositeProperty.isarType == IsarType.string || + compositeProperty.isarType == IsarType.stringList; + final defaultType = compositeProperty.isarType.isList || isString + ? IndexType.hash + : IndexType.value; + indexProperties.add( + ObjectIndexProperty( + property: compositeProperty, + type: c.type ?? defaultType, + caseSensitive: c.caseSensitive ?? isString, + ), + ); + } + } + + final name = index.name ?? + indexProperties.map((e) => e.property.isarName).join('_'); + checkIsarName(name, element); + + final objectIndex = ObjectIndex( + name: name, + properties: indexProperties, + unique: index.unique, + replace: index.replace, + ); + _verifyObjectIndex(objectIndex, element); + + yield objectIndex; + } + } + + void _verifyObjectIndex(ObjectIndex index, Element element) { + final properties = index.properties; + + if (properties.map((it) => it.property.isarName).distinct().length != + properties.length) { + err('Composite index contains duplicate properties.', element); + } + + for (var i = 0; i < properties.length; i++) { + final property = properties[i]; + if (property.isarType.isList && + property.type != IndexType.hash && + properties.length > 1) { + err('Composite indexes do not support non-hashed lists.', element); + } + if (property.isarType.containsFloat && i != properties.lastIndex) { + err( + 'Only the last property of a composite index may be a ' + 'double value.', + element, + ); + } + if (property.isarType == IsarType.string) { + if (property.type != IndexType.hash && i != properties.lastIndex) { + err( + 'Only the last property of a composite index may be a ' + 'non-hashed String.', + element, + ); + } + } + if (property.isarType.containsObject) { + err( + 'Embedded objects may not be indexed.', + element, + ); + } + if (property.type != IndexType.value) { + if (!property.isarType.isList && property.isarType != IsarType.string) { + err('Only Strings and Lists may be hashed.', element); + } else if (property.isarType.containsFloat) { + err('List may must not be hashed.', element); + } + } + if (property.isarType != IsarType.stringList && + property.type == IndexType.hashElements) { + err('Only String lists may have hashed elements.', element); + } + } + + if (!index.unique && index.replace) { + err('Only unique indexes can replace.', element); + } + } +} diff --git a/lib/src/isar_type.dart b/lib/src/isar_type.dart new file mode 100644 index 0000000..e1c3f1d --- /dev/null +++ b/lib/src/isar_type.dart @@ -0,0 +1,107 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:isar/isar.dart'; +import 'package:isar_generator/src/helper.dart'; +import 'package:source_gen/source_gen.dart'; + +const TypeChecker _dateTimeChecker = TypeChecker.fromRuntime(DateTime); +bool _isDateTime(Element element) => _dateTimeChecker.isExactly(element); + +extension DartTypeX on DartType { + IsarType? get _primitiveIsarType { + if (isDartCoreBool) { + return IsarType.bool; + } else if (isDartCoreInt) { + if (alias?.element.name == 'byte') { + return IsarType.byte; + } else if (alias?.element.name == 'short') { + return IsarType.int; + } else { + return IsarType.long; + } + } else if (isDartCoreDouble) { + if (alias?.element.name == 'float') { + return IsarType.float; + } else { + return IsarType.double; + } + } else if (isDartCoreString) { + return IsarType.string; + } else if (_isDateTime(element2!)) { + return IsarType.dateTime; + } else if (element2!.embeddedAnnotation != null) { + return IsarType.object; + } + + return null; + } + + bool get isIsarId { + return alias?.element.name == 'Id'; + } + + DartType get scalarType { + if (isDartCoreList) { + final parameterizedType = this as ParameterizedType; + final typeArguments = parameterizedType.typeArguments; + if (typeArguments.isNotEmpty) { + return typeArguments[0]; + } + } + return this; + } + + IsarType? get isarType { + final primitiveType = _primitiveIsarType; + if (primitiveType != null) { + return primitiveType; + } + + if (isDartCoreList) { + switch (scalarType._primitiveIsarType) { + case IsarType.bool: + return IsarType.boolList; + case IsarType.byte: + return IsarType.byteList; + case IsarType.int: + return IsarType.intList; + case IsarType.float: + return IsarType.floatList; + case IsarType.long: + return IsarType.longList; + case IsarType.double: + return IsarType.doubleList; + case IsarType.dateTime: + return IsarType.dateTimeList; + case IsarType.string: + return IsarType.stringList; + case IsarType.object: + return IsarType.objectList; + // ignore: no_default_cases + default: + return null; + } + } + + return null; + } +} + +extension IsarTypeX on IsarType { + bool get containsBool => this == IsarType.bool || this == IsarType.boolList; + + bool get containsFloat => + this == IsarType.float || + this == IsarType.floatList || + this == IsarType.double || + this == IsarType.doubleList; + + bool get containsDate => + this == IsarType.dateTime || this == IsarType.dateTimeList; + + bool get containsString => + this == IsarType.string || this == IsarType.stringList; + + bool get containsObject => + this == IsarType.object || this == IsarType.objectList; +} diff --git a/lib/src/object_info.dart b/lib/src/object_info.dart new file mode 100644 index 0000000..dff34e5 --- /dev/null +++ b/lib/src/object_info.dart @@ -0,0 +1,211 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dartx/dartx.dart'; +import 'package:isar/isar.dart'; + +import 'package:xxh3/xxh3.dart'; + +class ObjectInfo { + ObjectInfo({ + required this.dartName, + required this.isarName, + this.accessor, + required List properties, + this.embeddedDartNames = const {}, + this.indexes = const [], + this.links = const [], + }) { + this.properties = properties.sortedBy((e) => e.isarName).toList(); + } + + final String dartName; + final String isarName; + final String? accessor; + late final List properties; + final Map embeddedDartNames; + final List indexes; + final List links; + + int get id => xxh3(utf8.encode(isarName) as Uint8List); + + bool get isEmbedded => accessor == null; + + ObjectProperty get idProperty => properties.firstWhere((it) => it.isId); + + List get objectProperties => + properties.where((it) => !it.isId).toList(); + + String get getIdName => '_${dartName.decapitalize()}GetId'; + String get getLinksName => '_${dartName.decapitalize()}GetLinks'; + String get attachName => '_${dartName.decapitalize()}Attach'; + + String get estimateSizeName => '_${dartName.decapitalize()}EstimateSize'; + String get serializeName => '_${dartName.decapitalize()}Serialize'; + String get deserializeName => '_${dartName.decapitalize()}Deserialize'; + String get deserializePropName => + '_${dartName.decapitalize()}DeserializeProp'; +} + +enum PropertyDeser { + none, + assign, + positionalParam, + namedParam, +} + +class ObjectProperty { + ObjectProperty({ + required this.dartName, + required this.isarName, + required this.typeClassName, + this.targetIsarName, + required this.isarType, + required this.isId, + required this.enumMap, + required this.enumProperty, + required this.defaultEnumElement, + required this.nullable, + required this.elementNullable, + this.userDefaultValue, + required this.deserialize, + required this.assignable, + this.constructorPosition, + }); + + final String dartName; + final String isarName; + final String typeClassName; + final String? targetIsarName; + + final bool isId; + final IsarType isarType; + final Map? enumMap; + final String? enumProperty; + final String? defaultEnumElement; + + final bool nullable; + final bool elementNullable; + final String? userDefaultValue; + + final PropertyDeser deserialize; + final bool assignable; + final int? constructorPosition; + + bool get isEnum => enumMap != null; + + String get scalarDartType { + if (isId) { + return 'Id'; + } else if (isEnum) { + return typeClassName; + } + + switch (isarType) { + case IsarType.bool: + case IsarType.boolList: + return 'bool'; + case IsarType.byte: + case IsarType.byteList: + case IsarType.int: + case IsarType.intList: + case IsarType.long: + case IsarType.longList: + return 'int'; + case IsarType.float: + case IsarType.floatList: + case IsarType.double: + case IsarType.doubleList: + return 'double'; + case IsarType.dateTime: + case IsarType.dateTimeList: + return 'DateTime'; + case IsarType.object: + case IsarType.objectList: + return typeClassName; + case IsarType.string: + case IsarType.stringList: + return 'String'; + } + } + + String get nScalarDartType => isarType.isList + ? '$scalarDartType${elementNullable ? '?' : ''}' + : '$scalarDartType${nullable ? '?' : ''}'; + + String get dartType => isarType.isList + ? 'List<$nScalarDartType>${nullable ? '?' : ''}' + : nScalarDartType; + + String get targetSchema => '${scalarDartType.capitalize()}Schema'; + + String enumValueMapName(ObjectInfo object) { + return '_${object.dartName}${dartName}EnumValueMap'; + } + + String valueEnumMapName(ObjectInfo object) { + return '_${object.dartName}${dartName}ValueEnumMap'; + } +} + +class ObjectIndexProperty { + const ObjectIndexProperty({ + required this.property, + required this.type, + required this.caseSensitive, + }); + + final ObjectProperty property; + final IndexType type; + final bool caseSensitive; + + IsarType get isarType => property.isarType; + + bool get isMultiEntry => isarType.isList && type != IndexType.hash; +} + +class ObjectIndex { + ObjectIndex({ + required this.name, + required this.properties, + required this.unique, + required this.replace, + }); + + final String name; + final List properties; + final bool unique; + final bool replace; + + late final id = xxh3(utf8.encode(name) as Uint8List); +} + +class ObjectLink { + const ObjectLink({ + required this.dartName, + required this.isarName, + this.targetLinkIsarName, + required this.targetCollectionDartName, + required this.targetCollectionIsarName, + required this.isSingle, + }); + + final String dartName; + final String isarName; + + // isar name of the original link (only for backlinks) + final String? targetLinkIsarName; + final String targetCollectionDartName; + final String targetCollectionIsarName; + final bool isSingle; + + bool get isBacklink => targetLinkIsarName != null; + + int id(String objectIsarName) { + final col = isBacklink ? targetCollectionIsarName : objectIsarName; + final colId = xxh3(utf8.encode(col) as Uint8List, seed: isBacklink ? 1 : 0); + + final name = targetLinkIsarName ?? isarName; + return xxh3(utf8.encode(name) as Uint8List, seed: colId); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..0a8c2a1 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,26 @@ +name: isar_generator +description: Code generator for the Isar Database. Finds classes annotated with @Collection. +version: 3.1.0+1 +repository: https://github.com/isar/isar +homepage: https://isar.dev + +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + analyzer: ">=4.6.0 <6.0.0" + build: ^2.3.0 + dart_style: ^2.2.3 + dartx: ^1.1.0 + glob: ^2.0.2 + isar: + path: ../isar + path: ^1.8.1 + source_gen: ^1.2.2 + xxh3: ^1.0.1 + +dev_dependencies: + build_test: ^2.1.5 + matcher: ^0.12.12 + test: ^1.21.0 + very_good_analysis: ^3.0.1 diff --git a/test/error_test.dart b/test/error_test.dart new file mode 100644 index 0000000..3b90980 --- /dev/null +++ b/test/error_test.dart @@ -0,0 +1,33 @@ +import 'dart:io'; + +import 'package:build/build.dart'; +import 'package:build_test/build_test.dart'; +import 'package:isar_generator/isar_generator.dart'; +import 'package:test/test.dart'; + +void main() { + group('Error case', () { + for (final file in Directory('test/errors').listSync(recursive: true)) { + if (file is! File || !file.path.endsWith('.dart')) continue; + + test(file.path, () async { + final content = await file.readAsLines(); + + final errorMessage = content.first.split('//').last.trim(); + + var error = ''; + try { + await testBuilder( + getIsarGenerator(BuilderOptions.empty), + {'a|${file.path}': content.join('\n')}, + reader: await PackageAssetReader.currentIsolate(), + ); + } catch (e) { + error = e.toString(); + } + + expect(error.toLowerCase(), contains(errorMessage.toLowerCase())); + }); + } + }); +} diff --git a/test/errors/class/abstract.dart b/test/errors/class/abstract.dart new file mode 100644 index 0000000..b116529 --- /dev/null +++ b/test/errors/class/abstract.dart @@ -0,0 +1,8 @@ +// must not be abstract + +import 'package:isar/isar.dart'; + +@collection +abstract class Model { + Id? id; +} diff --git a/test/errors/class/collection_supertype.dart b/test/errors/class/collection_supertype.dart new file mode 100644 index 0000000..e6e3f8d --- /dev/null +++ b/test/errors/class/collection_supertype.dart @@ -0,0 +1,19 @@ +// supertype annotated with @collection + +import 'package:isar/isar.dart'; + +@collection +class Supertype { + Id? id; +} + +class Subtype implements Supertype { + @override + Id? id; +} + +@collection +class Model implements Subtype { + @override + Id? id; +} diff --git a/test/errors/class/constructor_named.dart b/test/errors/class/constructor_named.dart new file mode 100644 index 0000000..713f795 --- /dev/null +++ b/test/errors/class/constructor_named.dart @@ -0,0 +1,10 @@ +// unnamed constructor + +import 'package:isar/isar.dart'; + +@collection +class Model { + Model.create(); + + Id? id; +} diff --git a/test/errors/class/constructor_unknown_parameter.dart b/test/errors/class/constructor_unknown_parameter.dart new file mode 100644 index 0000000..768e349 --- /dev/null +++ b/test/errors/class/constructor_unknown_parameter.dart @@ -0,0 +1,13 @@ +// constructor parameter does not match a property + +import 'package:isar/isar.dart'; + +@collection +class Model { + // ignore: avoid_unused_constructor_parameters + Model(this.prop1, String somethingElse); + + Id? id; + + final String prop1; +} diff --git a/test/errors/class/constructor_wrong_parameter.dart b/test/errors/class/constructor_wrong_parameter.dart new file mode 100644 index 0000000..1274a5c --- /dev/null +++ b/test/errors/class/constructor_wrong_parameter.dart @@ -0,0 +1,13 @@ +// constructor parameter type does not match property type + +import 'package:isar/isar.dart'; + +@collection +class Model { + // ignore: avoid_unused_constructor_parameters + Model(int prop1); + + Id? id; + + String prop1 = '5'; +} diff --git a/test/errors/class/enum.dart b/test/errors/class/enum.dart new file mode 100644 index 0000000..a51d088 --- /dev/null +++ b/test/errors/class/enum.dart @@ -0,0 +1,7 @@ +// only classes + +import 'package:isar/isar.dart'; + +// ignore: invalid_annotation_target +@collection +enum Test { a, b, c } diff --git a/test/errors/class/invalid_name.dart b/test/errors/class/invalid_name.dart new file mode 100644 index 0000000..b116529 --- /dev/null +++ b/test/errors/class/invalid_name.dart @@ -0,0 +1,8 @@ +// must not be abstract + +import 'package:isar/isar.dart'; + +@collection +abstract class Model { + Id? id; +} diff --git a/test/errors/class/mixin.dart b/test/errors/class/mixin.dart new file mode 100644 index 0000000..a6a6374 --- /dev/null +++ b/test/errors/class/mixin.dart @@ -0,0 +1,7 @@ +// only classes + +import 'package:isar/isar.dart'; + +// ignore: invalid_annotation_target +@collection +mixin Test {} diff --git a/test/errors/class/private.dart b/test/errors/class/private.dart new file mode 100644 index 0000000..aa545b9 --- /dev/null +++ b/test/errors/class/private.dart @@ -0,0 +1,9 @@ +// must be public + +import 'package:isar/isar.dart'; + +@collection +// ignore: unused_element +class _Model { + Id? id; +} diff --git a/test/errors/class/variable.dart b/test/errors/class/variable.dart new file mode 100644 index 0000000..543a78e --- /dev/null +++ b/test/errors/class/variable.dart @@ -0,0 +1,7 @@ +// only classes + +import 'package:isar/isar.dart'; + +// ignore: invalid_annotation_target +@collection +const t = 'hello'; diff --git a/test/errors/id/duplicate.dart b/test/errors/id/duplicate.dart new file mode 100644 index 0000000..e96e21f --- /dev/null +++ b/test/errors/id/duplicate.dart @@ -0,0 +1,10 @@ +// two or more properties with type "Id" defined + +import 'package:isar/isar.dart'; + +@collection +class Test { + Id? id1; + + Id? id2; +} diff --git a/test/errors/id/missing.dart b/test/errors/id/missing.dart new file mode 100644 index 0000000..e414174 --- /dev/null +++ b/test/errors/id/missing.dart @@ -0,0 +1,10 @@ +// no id property defined + +import 'package:isar/isar.dart'; + +@collection +class Test { + late int id; + + late String name; +} diff --git a/test/errors/index/composite_double_not_last.dart b/test/errors/index/composite_double_not_last.dart new file mode 100644 index 0000000..135b56b --- /dev/null +++ b/test/errors/index/composite_double_not_last.dart @@ -0,0 +1,13 @@ +// only the last property of a composite index may be a double value + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(composite: [CompositeIndex('val2')]) + double? val1; + + String? val2; +} diff --git a/test/errors/index/composite_non_hashed_list.dart b/test/errors/index/composite_non_hashed_list.dart new file mode 100644 index 0000000..e93241e --- /dev/null +++ b/test/errors/index/composite_non_hashed_list.dart @@ -0,0 +1,13 @@ +// composite indexes do not support non-hashed lists + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(composite: [CompositeIndex('str')], type: IndexType.value) + List? list; + + String? str; +} diff --git a/test/errors/index/composite_string_value_not_last.dart b/test/errors/index/composite_string_value_not_last.dart new file mode 100644 index 0000000..52efbf8 --- /dev/null +++ b/test/errors/index/composite_string_value_not_last.dart @@ -0,0 +1,13 @@ +// last property of a composite index may be a non-hashed string + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(composite: [CompositeIndex('str2')], type: IndexType.value) + String? str1; + + String? str2; +} diff --git a/test/errors/index/contains_id.dart b/test/errors/index/contains_id.dart new file mode 100644 index 0000000..fd45fb2 --- /dev/null +++ b/test/errors/index/contains_id.dart @@ -0,0 +1,11 @@ +// ids cannot be indexed + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(composite: [CompositeIndex('id')]) + String? str; +} diff --git a/test/errors/index/double_list_hashed.dart b/test/errors/index/double_list_hashed.dart new file mode 100644 index 0000000..84563d4 --- /dev/null +++ b/test/errors/index/double_list_hashed.dart @@ -0,0 +1,11 @@ +// list may must not be hashed + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(type: IndexType.hash) + List? list; +} diff --git a/test/errors/index/duplicate_name.dart b/test/errors/index/duplicate_name.dart new file mode 100644 index 0000000..17e6cab --- /dev/null +++ b/test/errors/index/duplicate_name.dart @@ -0,0 +1,14 @@ +// same name + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(name: 'myindex') + String? prop1; + + @Index(name: 'myindex') + String? prop2; +} diff --git a/test/errors/index/duplicate_property.dart b/test/errors/index/duplicate_property.dart new file mode 100644 index 0000000..01c53f0 --- /dev/null +++ b/test/errors/index/duplicate_property.dart @@ -0,0 +1,13 @@ +// composite index contains duplicate properties + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(composite: [CompositeIndex('str1')], type: IndexType.value) + String? str1; + + String? str2; +} diff --git a/test/errors/index/invalid_name.dart b/test/errors/index/invalid_name.dart new file mode 100644 index 0000000..171fe3a --- /dev/null +++ b/test/errors/index/invalid_name.dart @@ -0,0 +1,11 @@ +// names must not be blank or start with "_" + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(name: '_index') + String? str; +} diff --git a/test/errors/index/non_string_hashed.dart b/test/errors/index/non_string_hashed.dart new file mode 100644 index 0000000..63267d6 --- /dev/null +++ b/test/errors/index/non_string_hashed.dart @@ -0,0 +1,11 @@ +// only strings and lists may be hashed + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(type: IndexType.hash) + int? val; +} diff --git a/test/errors/index/non_string_list_hashed_elements.dart b/test/errors/index/non_string_list_hashed_elements.dart new file mode 100644 index 0000000..4f119fc --- /dev/null +++ b/test/errors/index/non_string_list_hashed_elements.dart @@ -0,0 +1,11 @@ +// only string lists may have hashed elements + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(type: IndexType.hashElements) + List? list; +} diff --git a/test/errors/index/non_unique_replace.dart b/test/errors/index/non_unique_replace.dart new file mode 100644 index 0000000..9a49b37 --- /dev/null +++ b/test/errors/index/non_unique_replace.dart @@ -0,0 +1,11 @@ +// only unique indexes can replace + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(replace: true) + String? str; +} diff --git a/test/errors/index/object_hashed.dart b/test/errors/index/object_hashed.dart new file mode 100644 index 0000000..dd20d11 --- /dev/null +++ b/test/errors/index/object_hashed.dart @@ -0,0 +1,14 @@ +// objects may not be indexed + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index() + EmbeddedModel? obj; +} + +@embedded +class EmbeddedModel {} diff --git a/test/errors/index/object_list_hashed.dart b/test/errors/index/object_list_hashed.dart new file mode 100644 index 0000000..d149e19 --- /dev/null +++ b/test/errors/index/object_list_hashed.dart @@ -0,0 +1,14 @@ +// objects may not be indexed + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(type: IndexType.hash) + List? list; +} + +@embedded +class EmbeddedModel {} diff --git a/test/errors/index/property_does_not_exist.dart b/test/errors/index/property_does_not_exist.dart new file mode 100644 index 0000000..806515e --- /dev/null +++ b/test/errors/index/property_does_not_exist.dart @@ -0,0 +1,11 @@ +// property does not exist + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Index(composite: [CompositeIndex('myProp')]) + String? str; +} diff --git a/test/errors/link/backlink_target_does_no_exist.dart b/test/errors/link/backlink_target_does_no_exist.dart new file mode 100644 index 0000000..ea285c2 --- /dev/null +++ b/test/errors/link/backlink_target_does_no_exist.dart @@ -0,0 +1,16 @@ +// target of backlink does not exist + +import 'package:isar/isar.dart'; + +@collection +class Model1 { + Id? id; + + @Backlink(to: 'abc') + final IsarLink link = IsarLink(); +} + +@collection +class Model2 { + Id? id; +} diff --git a/test/errors/link/backlink_target_is_backlink.dart b/test/errors/link/backlink_target_is_backlink.dart new file mode 100644 index 0000000..5d5f48d --- /dev/null +++ b/test/errors/link/backlink_target_is_backlink.dart @@ -0,0 +1,19 @@ +// target of backlink is also a backlink + +import 'package:isar/isar.dart'; + +@collection +class Model1 { + Id? id; + + @Backlink(to: 'link') + final IsarLink link = IsarLink(); +} + +@collection +class Model2 { + Id? id; + + @Backlink(to: 'link') + final IsarLink link = IsarLink(); +} diff --git a/test/errors/link/backlink_target_not_a_link.dart b/test/errors/link/backlink_target_not_a_link.dart new file mode 100644 index 0000000..459ecd3 --- /dev/null +++ b/test/errors/link/backlink_target_not_a_link.dart @@ -0,0 +1,18 @@ +// target of backlink is not a link + +import 'package:isar/isar.dart'; + +@collection +class Model1 { + Id? id; + + @Backlink(to: 'str') + final IsarLink link = IsarLink(); +} + +@collection +class Model2 { + Id? id; + + String? str; +} diff --git a/test/errors/link/duplicate_name.dart b/test/errors/link/duplicate_name.dart new file mode 100644 index 0000000..1405c4a --- /dev/null +++ b/test/errors/link/duplicate_name.dart @@ -0,0 +1,18 @@ +// same name + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + final IsarLink prop1 = IsarLink(); + + @Name('prop1') + final IsarLinks prop2 = IsarLinks(); +} + +@collection +class Model2 { + Id? id; +} diff --git a/test/errors/link/invalid_name.dart b/test/errors/link/invalid_name.dart new file mode 100644 index 0000000..47b89a9 --- /dev/null +++ b/test/errors/link/invalid_name.dart @@ -0,0 +1,16 @@ +// names must not be blank or start with + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Name('_link') + final IsarLink link = IsarLink(); +} + +@collection +class Model2 { + Id? id; +} diff --git a/test/errors/link/late.dart b/test/errors/link/late.dart new file mode 100644 index 0000000..845cf27 --- /dev/null +++ b/test/errors/link/late.dart @@ -0,0 +1,15 @@ +// must not be late + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + late IsarLink link; +} + +@collection +class Model2 { + Id? id; +} diff --git a/test/errors/link/nullable.dart b/test/errors/link/nullable.dart new file mode 100644 index 0000000..ab1f588 --- /dev/null +++ b/test/errors/link/nullable.dart @@ -0,0 +1,15 @@ +// must not be nullable + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + IsarLink? link; +} + +@collection +class Model2 { + Id? id; +} diff --git a/test/errors/link/target_not_a_collection.dart b/test/errors/link/target_not_a_collection.dart new file mode 100644 index 0000000..dd7d3cd --- /dev/null +++ b/test/errors/link/target_not_a_collection.dart @@ -0,0 +1,10 @@ +// link target is not annotated with @collection + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + final IsarLink link = IsarLink(); +} diff --git a/test/errors/link/type_nullable.dart b/test/errors/link/type_nullable.dart new file mode 100644 index 0000000..865d421 --- /dev/null +++ b/test/errors/link/type_nullable.dart @@ -0,0 +1,15 @@ +// links type must not be nullable + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + final IsarLink link = IsarLink(); +} + +@collection +class Model2 { + Id? id; +} diff --git a/test/errors/property/duplicate_name.dart b/test/errors/property/duplicate_name.dart new file mode 100644 index 0000000..0b275cb --- /dev/null +++ b/test/errors/property/duplicate_name.dart @@ -0,0 +1,13 @@ +// same name + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + String? prop1; + + @Name('prop1') + String? prop2; +} diff --git a/test/errors/property/enum_bool_type.dart b/test/errors/property/enum_bool_type.dart new file mode 100644 index 0000000..d5b4b44 --- /dev/null +++ b/test/errors/property/enum_bool_type.dart @@ -0,0 +1,17 @@ +// unsupported enum property type + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Enumerated(EnumType.value, 'value') + late MyEnum field; +} + +enum MyEnum { + optionA; + + final bool value = true; +} diff --git a/test/errors/property/enum_double_type.dart b/test/errors/property/enum_double_type.dart new file mode 100644 index 0000000..17567e6 --- /dev/null +++ b/test/errors/property/enum_double_type.dart @@ -0,0 +1,17 @@ +// unsupported enum property type + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Enumerated(EnumType.value, 'value') + late MyEnum field; +} + +enum MyEnum { + optionA; + + final double value = 5.5; +} diff --git a/test/errors/property/enum_duplicate.dart b/test/errors/property/enum_duplicate.dart new file mode 100644 index 0000000..32ce056 --- /dev/null +++ b/test/errors/property/enum_duplicate.dart @@ -0,0 +1,21 @@ +// has duplicate values + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Enumerated(EnumType.value, 'value') + late MyEnum field; +} + +enum MyEnum { + option1(1), + option2(2), + option3(1); + + const MyEnum(this.value); + + final int value; +} diff --git a/test/errors/property/enum_float_type.dart b/test/errors/property/enum_float_type.dart new file mode 100644 index 0000000..abf3830 --- /dev/null +++ b/test/errors/property/enum_float_type.dart @@ -0,0 +1,17 @@ +// unsupported enum property type + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Enumerated(EnumType.value, 'value') + late MyEnum field; +} + +enum MyEnum { + optionA; + + final float value = 5.5; +} diff --git a/test/errors/property/enum_list_type.dart b/test/errors/property/enum_list_type.dart new file mode 100644 index 0000000..75d78e2 --- /dev/null +++ b/test/errors/property/enum_list_type.dart @@ -0,0 +1,17 @@ +// unsupported enum property type + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Enumerated(EnumType.value, 'value') + late MyEnum prop; +} + +enum MyEnum { + optionA; + + final List value = []; +} diff --git a/test/errors/property/enum_not_annotated.dart b/test/errors/property/enum_not_annotated.dart new file mode 100644 index 0000000..456e8e4 --- /dev/null +++ b/test/errors/property/enum_not_annotated.dart @@ -0,0 +1,14 @@ +// enum property must be annotated with @enumerated + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + late MyEnum? prop; +} + +enum MyEnum { + a; +} diff --git a/test/errors/property/enum_null_value.dart b/test/errors/property/enum_null_value.dart new file mode 100644 index 0000000..bdc2e30 --- /dev/null +++ b/test/errors/property/enum_null_value.dart @@ -0,0 +1,17 @@ +// null values are not supported + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Enumerated(EnumType.value, 'value') + late MyEnum prop; +} + +enum MyEnum { + optionA; + + final String? value = null; +} diff --git a/test/errors/property/enum_object_type.dart b/test/errors/property/enum_object_type.dart new file mode 100644 index 0000000..afa6d96 --- /dev/null +++ b/test/errors/property/enum_object_type.dart @@ -0,0 +1,20 @@ +// unsupported enum property type + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Enumerated(EnumType.value, 'value') + late MyEnum prop; +} + +enum MyEnum { + optionA; + + final value = EmbeddedModel(); +} + +@embedded +class EmbeddedModel {} diff --git a/test/errors/property/invalid_name.dart b/test/errors/property/invalid_name.dart new file mode 100644 index 0000000..5bc5341 --- /dev/null +++ b/test/errors/property/invalid_name.dart @@ -0,0 +1,11 @@ +// names must not be blank or start with "_" + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + @Name('_prop') + String? prop; +} diff --git a/test/errors/property/null_byte.dart b/test/errors/property/null_byte.dart new file mode 100644 index 0000000..c581380 --- /dev/null +++ b/test/errors/property/null_byte.dart @@ -0,0 +1,10 @@ +// bytes must not be nullable + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + late byte? prop; +} diff --git a/test/errors/property/null_byte_element.dart b/test/errors/property/null_byte_element.dart new file mode 100644 index 0000000..d45c4b1 --- /dev/null +++ b/test/errors/property/null_byte_element.dart @@ -0,0 +1,10 @@ +// bytes must not be nullable + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + late List prop; +} diff --git a/test/errors/property/unsupported_type.dart b/test/errors/property/unsupported_type.dart new file mode 100644 index 0000000..68af2cf --- /dev/null +++ b/test/errors/property/unsupported_type.dart @@ -0,0 +1,10 @@ +// unsupported type + +import 'package:isar/isar.dart'; + +@collection +class Model { + Id? id; + + late Set? prop; +}