dart_neo4j_ogm 1.0.0 copy "dart_neo4j_ogm: ^1.0.0" to clipboard
dart_neo4j_ogm: ^1.0.0 copied to clipboard

Annotations for Neo4j Object-Graph Mapping (OGM) code generation in Dart.

dart_neo4j_ogm #

A Neo4j Object-Graph Mapping (OGM) system for Dart that provides compile-time code generation for converting Dart classes to Cypher queries. This package works with dart_neo4j_ogm_generator to automatically generate extension methods that simplify Neo4j database interactions.

Features #

  • 🚀 Compile-time code generation - No runtime reflection
  • 🎯 Type-safe - Full compile-time type checking
  • 🔧 Customizable - Control field mapping and ignore fields
  • ❄️ Freezed compatible - Works seamlessly with Freezed classes
  • 📦 Minimal dependencies - Only annotation classes, no runtime overhead

Installation #

Add both packages to your pubspec.yaml:

dependencies:
  dart_neo4j_ogm: <lastest-version>

dev_dependencies:
  dart_neo4j_ogm_generator: <latest-version>
  build_runner: <latest-version>

Basic Usage #

1. Annotate your classes #

import 'package:dart_neo4j_ogm/dart_neo4j_ogm.dart';

part 'user.cypher.dart';

@cypherNode
class User {
  // Required: All @cypherNode classes must have an ID field
  // Use CypherElementId for Neo4j 5.0+ (recommended)
  final CypherElementId elementId;
  final String name;
  final String email;

  const User({
    required this.elementId,
    required this.name,
    required this.email,
  });

  factory User.fromNode(Node node) => _$UserFromNode(node);
}

Note: For Neo4j 4.x or legacy code, you can use CypherId (deprecated):

@cypherNode
class User {
  final CypherId id;  // Deprecated: Use CypherElementId for Neo4j 5.0+
  final String name;
  final String email;

  const User({
    required this.id,
    required this.name,
    required this.email,
  });

  factory User.fromNode(Node node) => _$UserFromNode(node);
}

2. Run code generation #

dart run build_runner build

3. Use the generated methods #

import 'package:dart_neo4j/dart_neo4j.dart';

// Create a user instance (elementId will be set by Neo4j when creating nodes)
final user = User(
  elementId: CypherElementId.none(),  // No elementId for new nodes - Neo4j will generate one
  name: 'John Doe',
  email: '[email protected]',
);

// Use generated methods with Neo4j
final driver = Neo4jDriver.create('bolt://localhost:7687');
final session = driver.session();

// Create a node - elementId field is automatically excluded from properties
await session.run(
  'CREATE ${user.toCypherWithPlaceholders('u')} RETURN u',
  user.cypherParameters,
);

// The above generates: 'CREATE (u:User {name: $name, email: $email}) RETURN u'
// With parameters: {'name': 'John Doe', 'email': '[email protected]'}
// Note: elementId is automatically excluded from Cypher properties

// Read nodes back using the fromNode factory
final result = await session.run('MATCH (u:User) RETURN u LIMIT 1');
final record = result.records.first;
final node = record['u'] as Node;
final userFromDb = User.fromNode(node);  // elementId from node.elementId, properties from node.properties

print('User Element ID from Neo4j: ${userFromDb.elementId}');  // Neo4j generated elementId
print('User name: ${userFromDb.name}');

await session.close();
await driver.close();

Advanced Usage #

ID Field Requirements #

All @cypherNode classes must have at least one ID field:

  • Recommended (Neo4j 5.0+): Use CypherElementId for string-based element IDs
  • Legacy (Neo4j 4.x): Use CypherId for numeric IDs (deprecated)
  • Migration: You can have both types during migration

ID fields can have any name (e.g., id, elementId, nodeId, uid). The generator identifies ID fields by their type, not their name.

// Neo4j 5.0+ style (recommended)
@cypherNode
class User {
  final CypherElementId elementId;  // String-based element ID
  final String name;

  const User({required this.elementId, required this.name});
  factory User.fromNode(Node node) => _$UserFromNode(node);
}

// Legacy style (still supported but deprecated)
@cypherNode
class LegacyUser {
  final CypherId id;  // Numeric ID (deprecated)
  final String name;

  const LegacyUser({required this.id, required this.name});
  factory LegacyUser.fromNode(Node node) => _$LegacyUserFromNode(node);
}

// Migration style (both IDs during transition)
@cypherNode
class HybridUser {
  final CypherId legacyId;          // For backward compatibility
  final CypherElementId elementId;   // For Neo4j 5.0+ compatibility
  final String name;

  const HybridUser({
    required this.legacyId,
    required this.elementId,
    required this.name,
  });
  factory HybridUser.fromNode(Node node) => _$HybridUserFromNode(node);
}

Custom Labels and Property Names #

@CypherNode(label: 'Person')  // Custom Neo4j label
class Customer {
  final CypherElementId elementId;  // ID field (automatically excluded from Cypher properties)

  @CypherProperty(name: 'fullName')  // Custom property name in Neo4j
  final String name;

  @CypherProperty(ignore: true)  // Exclude from Cypher generation
  final String internalCode;

  final double? price;  // Nullable fields are handled automatically

  const Customer({
    required this.elementId,
    required this.name,
    required this.internalCode,
    this.price,
  });

  factory Customer.fromNode(Node node) => _$CustomerFromNode(node);
}

Generated usage:

final customer = Customer(
  elementId: CypherElementId.none(),  // No elementId for new nodes
  name: 'Jane Smith',
  internalCode: 'INTERNAL_123',
  price: 99.99,
);

print(customer.nodeLabel);  // 'Person'
print(customer.cypherParameters);
// {'fullName': 'Jane Smith', 'price': 99.99}
// Note: elementId is automatically excluded, internalCode is excluded due to @CypherProperty(ignore: true)

print(customer.toCypherWithPlaceholders('c'));
// '(c:Person {fullName: $fullName, price: $price})'
// Note: ID fields are automatically excluded from Cypher properties

Freezed Integration #

This package works seamlessly with Freezed classes. You need to configure your build system to run Freezed before the OGM generator.

Build Configuration #

Create or update your build.yaml file:

targets:
  $default:
    builders:
      freezed:freezed:
        runs_before: ['dart_neo4j_ogm_generator:cypher_generator']
      dart_neo4j_ogm_generator:cypher_generator:
        enabled: true

Freezed Class Example #

import 'package:dart_neo4j_ogm/dart_neo4j_ogm.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.cypher.dart';

@freezed
@cypherNode
class User with _$User {
  const factory User({
    required CypherElementId elementId,  // ID field (automatically excluded from Cypher properties)
    required String name,

    @CypherProperty(name: 'emailAddress')
    required String email,

    @CypherProperty(ignore: true)
    required String password,

    String? bio,
  }) = _User;

  factory User.fromNode(Node node) => _$UserFromNode(node);
}

Usage with Freezed:

final user = User(
  elementId: CypherElementId.none(),  // No elementId for new nodes
  name: 'Alice Johnson',
  email: '[email protected]',
  password: 'secret123',
  bio: 'Software Developer',
);

print(user.cypherParameters);
// {'name': 'Alice Johnson', 'emailAddress': '[email protected]', 'bio': 'Software Developer'}
// Note: elementId is automatically excluded, password is excluded due to @CypherProperty(ignore: true)

print(user.toCypherWithPlaceholders('u'));
// '(u:User {name: $name, emailAddress: $emailAddress, bio: $bio})'
// Note: ID fields are automatically excluded from Cypher properties

JSON Serialization with Freezed + json_serializable #

The OGM system works seamlessly with Freezed classes that also use json_serializable for JSON serialization. This is particularly useful when you need to serialize your Neo4j entities to/from JSON for APIs or storage.

Build Configuration for JSON Support #

When using both Freezed and json_serializable, update your build.yaml to ensure proper build order:

targets:
  $default:
    builders:
      freezed:freezed:
        runs_before: ['dart_neo4j_ogm_generator:cypher_generator']
      json_serializable:json_serializable:
        runs_before: ['dart_neo4j_ogm_generator:cypher_generator']
      dart_neo4j_ogm_generator:cypher_generator:
        enabled: true

ID JSON Serialization #

The package provides built-in helper functions for ID JSON serialization:

For CypherElementId (recommended):

  • cypherElementIdToJson(CypherElementId elementId) - Converts to JSON (String? value)
  • cypherElementIdFromJson(String? json) - Creates from JSON value

For CypherId (deprecated):

  • cypherIdToJson(CypherId id) - Converts to JSON (int? value)
  • cypherIdFromJson(int? json) - Creates from JSON value

Complete JSON + Neo4j Example #

import 'package:dart_neo4j_ogm/dart_neo4j_ogm.dart';
import 'package:dart_neo4j/dart_neo4j.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'json_user.freezed.dart';
part 'json_user.g.dart';
part 'json_user.cypher.dart';

@freezed
@CypherNode()
class JsonUser with _$JsonUser {
  const factory JsonUser({
    @JsonKey(toJson: cypherElementIdToJson, fromJson: cypherElementIdFromJson)
    required CypherElementId elementId,
    required String name,
    required String email,
    @CypherProperty(name: 'userAge') int? age,
    @CypherProperty(ignore: true) String? internalNotes,
  }) = _JsonUser;

  factory JsonUser.fromJson(Map<String, dynamic> json) =>
      _$JsonUserFromJson(json);

  factory JsonUser.fromNode(Node node) => _$JsonUserFromNode(node);
}

For legacy CypherId (deprecated):

@freezed
@CypherNode()
class LegacyJsonUser with _$LegacyJsonUser {
  const factory LegacyJsonUser({
    @JsonKey(toJson: cypherIdToJson, fromJson: cypherIdFromJson)
    required CypherId id,  // Deprecated: Use CypherElementId
    required String name,
    required String email,
  }) = _LegacyJsonUser;

  factory LegacyJsonUser.fromJson(Map<String, dynamic> json) =>
      _$LegacyJsonUserFromJson(json);

  factory LegacyJsonUser.fromNode(Node node) => _$LegacyJsonUserFromNode(node);
}

Usage with JSON and Neo4j #

Future<void> jsonExample() async {
  // 1. Create from JSON (e.g., from API request)
  final jsonData = {
    'elementId': '4:abc123:42',
    'name': 'John Doe',
    'email': '[email protected]',
    'userAge': 30,
    'internalNotes': 'This will be ignored in Cypher'
  };

  final user = JsonUser.fromJson(jsonData);
  print('User from JSON: \${user.name}, Age: \${user.age}');

  // 2. Use with Neo4j (internalNotes is ignored due to @CypherProperty(ignore: true))
  final driver = Neo4jDriver.create('bolt://localhost:7687');
  final session = driver.session();

  try {
    // Create in Neo4j - only name, email, and userAge are included
    await session.run(
      'CREATE \${user.toCypherWithPlaceholders('u')} RETURN u',
      user.cypherParameters,
    );
    // Generated: CREATE (u:JsonUser {name: \$name, email: \$email, userAge: \$userAge}) RETURN u

    // 3. Read from Neo4j
    final result = await session.run('MATCH (u:JsonUser) RETURN u LIMIT 1');
    final node = result.records.first['u'] as Node;
    final userFromDb = JsonUser.fromNode(node);

    // 4. Convert back to JSON (e.g., for API response)
    final jsonResponse = userFromDb.toJson();
    print('User as JSON: \$jsonResponse');
    // Output: {elementId: '4:abc123:99', name: John Doe, email: [email protected], userAge: 30, internalNotes: null}

  } finally {
    await session.close();
    await driver.close();
  }
}

Key Benefits #

  1. Dual Serialization: Objects can be serialized to both JSON (for APIs) and Cypher (for Neo4j)
  2. Field Control: Use @CypherProperty(ignore: true) for JSON-only fields that shouldn't go to Neo4j
  3. Custom Mapping: Use @CypherProperty(name: 'customName') for different property names in Neo4j vs JSON
  4. Type Safety: Full compile-time type checking for both JSON and Cypher operations
  5. ID Handling: Proper CypherId serialization with custom JSON converters

Dependencies #

Add these to your pubspec.yaml:

dependencies:
  dart_neo4j_ogm: <latest-version>
  freezed_annotation: <latest-version>
  json_annotation: <latest-version>

dev_dependencies:
  dart_neo4j_ogm_generator: <latest-version>
  build_runner: <latest-version>
  freezed: <latest-version>
  json_serializable: <latest-version>

Avoiding Parameter Name Collisions #

When working with complex queries involving multiple nodes, parameter names can collide. The OGM provides prefixed methods to solve this:

final user = User(elementId: CypherElementId.none(), name: 'John', email: '[email protected]');
final post = BlogPost(elementId: CypherElementId.none(), title: 'Hello World', content: 'My first post', name: 'John');

// Without prefixes - parameter collision on 'name'!
// This would cause issues: {...user.cypherParameters, ...post.cypherParameters}
// Both objects have a 'name' field, causing parameter collision

// With prefixes - no collisions
await session.run('''
  CREATE ${user.toCypherWithPlaceholdersWithPrefix('u', 'user_')}
  CREATE ${post.toCypherWithPlaceholdersWithPrefix('p', 'post_')}
  CREATE (u)-[:AUTHORED]->(p)
''', {
  ...user.cypherParametersWithPrefix('user_'),
  ...post.cypherParametersWithPrefix('post_'),
});

// Generated query (note: ID fields are automatically excluded):
// CREATE (u:User {name: $user_name, email: $user_email})
// CREATE (p:BlogPost {title: $post_title, content: $post_content, name: $post_name})
// CREATE (u)-[:AUTHORED]->(p)

// With parameters:
// {
//   'user_name': 'John', 'user_email': '[email protected]',
//   'post_title': 'Hello World', 'post_content': 'My first post', 'post_name': 'John'
// }

Generated API Reference #

The code generator creates extension methods on your annotated classes:

Properties #

  • cypherParameters - Map<String, dynamic> containing field values for Cypher queries (excludes ID fields)
  • cypherProperties - String containing Cypher node properties syntax with parameter placeholders (e.g., {name: $name, email: $email})
  • nodeLabel - String containing the Neo4j node label (from annotation or class name)
  • cypherPropertyNames - List<String> containing the property names used in Cypher

Methods #

  • toCypherMap() - Returns the same as cypherParameters (alias for consistency)
  • toCypherWithPlaceholders(String variableName) - Returns complete Cypher node syntax with variable name, label, and properties (e.g., (u:User {name: $name, email: $email}))
  • cypherPropertiesWithPrefix(String prefix) - Returns Cypher properties string with prefixed parameter placeholders (e.g., {name: $user_name, email: $user_email})
  • cypherParametersWithPrefix(String prefix) - Returns parameter map with prefixed keys to avoid name collisions (e.g., {'user_name': 'John', 'user_email': '[email protected]'})
  • toCypherWithPlaceholdersWithPrefix(String variableName, String prefix) - Returns complete Cypher node syntax with prefixed parameter placeholders (e.g., (u:User {name: $user_name, email: $user_email}))

Factory Methods #

The generator creates static factory methods for creating instances from Neo4j Node objects:

  • fromNode(Node node) - Creates an instance from a Neo4j Node object
    • For CypherElementId fields: extracts from node.elementIdOrThrow
    • For CypherId fields (deprecated): extracts from node.id
    • Other properties extracted from node.properties

Annotations Reference #

@cypherNode / @CypherNode #

Marks a class for Cypher code generation.

@cypherNode  // Uses class name as label
// or
@CypherNode(label: 'CustomLabel')  // Uses custom label

Parameters:

  • label (optional): Custom Neo4j node label. Defaults to class name.

@CypherProperty #

Controls how individual fields are handled in Cypher generation.

@CypherProperty(
  ignore: false,  // Whether to exclude this field (default: false)
  name: null,     // Custom property name in Neo4j (default: field name)
)

Parameters:

  • ignore (optional): Set to true to exclude the field from Cypher generation
  • name (optional): Custom property name to use in Neo4j instead of the field name

Examples #

Complete Neo4j Integration Example #

import 'package:dart_neo4j/dart_neo4j.dart';
import 'package:dart_neo4j_ogm/dart_neo4j_ogm.dart';

part 'models.cypher.dart';

@cypherNode
class User {
  final CypherElementId elementId;  // ID field (any name allowed)
  final String name;
  final String email;

  const User({required this.elementId, required this.name, required this.email});

  factory User.fromNode(Node node) => _$UserFromNode(node);
}

@CypherNode(label: 'Post')
class BlogPost {
  final CypherElementId elementId;  // ID field (any name allowed)
  final String title;
  final String content;
  final String? authorElementId;  // Store as String for Neo4j 5.0+ compatibility

  const BlogPost({
    required this.elementId,
    required this.title,
    required this.content,
    this.authorElementId,
  });

  factory BlogPost.fromNode(Node node) => _$BlogPostFromNode(node);
}

// Usage
Future<void> example() async {
  final driver = Neo4jDriver.create('bolt://localhost:7687');
  final session = driver.session();

  final user = User(elementId: CypherElementId.none(), name: 'John', email: '[email protected]');
  final post = BlogPost(
    elementId: CypherElementId.none(),
    title: 'Hello World',
    content: 'My first post',
    authorElementId: null,  // Will be set to actual user elementId after creation
  );

  try {
    // Option 1: Create user first, then create post with relationship
    final userResult = await session.run(
      'CREATE ${user.toCypherWithPlaceholders('u')} RETURN u',
      user.cypherParameters,
    );
    final createdUserNode = userResult.records.first['u'] as Node;
    final createdUser = User.fromNode(createdUserNode);

    // Update post with actual user elementId
    final postWithUserId = BlogPost(
      elementId: CypherElementId.none(),
      title: post.title,
      content: post.content,
      authorElementId: createdUser.elementId.elementIdOrThrow,
    );

    await session.run('''
      MATCH (u:User) WHERE elementId(u) = \$authorElementId
      CREATE ${postWithUserId.toCypherWithPlaceholders('p')}
      CREATE (u)-[:AUTHORED]->(p)
    ''', postWithUserId.cypherParameters);

    // Option 2: Create both nodes with prefixed parameters (no relationship)
    final newUser = User(elementId: CypherElementId.none(), name: 'Jane', email: '[email protected]');
    final newPost = BlogPost(
      elementId: CypherElementId.none(),
      title: 'Another Post',
      content: 'Different content',
      authorElementId: null,
    );

    await session.run('''
      CREATE ${newUser.toCypherWithPlaceholdersWithPrefix('u', 'user_')}
      CREATE ${newPost.toCypherWithPlaceholdersWithPrefix('p', 'post_')}
    ''', {
      ...newUser.cypherParametersWithPrefix('user_'),
      ...newPost.cypherParametersWithPrefix('post_'),
    });

    // The above generates (note: elementId fields are automatically excluded):
    // CREATE (u:User {name: $user_name, email: $user_email})
    // CREATE (p:Post {title: $post_title, content: $post_content, authorElementId: $post_authorElementId})
    //
    // With parameters: {
    //   'user_name': 'Jane', 'user_email': '[email protected]',
    //   'post_title': 'Another Post', 'post_content': 'Different content', 'post_authorElementId': null
    // }

    // Query with results using fromNode factories
    final result = await session.run(
      'MATCH (u:User)-[:AUTHORED]->(p:Post) RETURN u, p',
    );

    // Read results using fromNode factories
    await for (final record in result) {
      final userNode = record['u'] as Node;
      final postNode = record['p'] as Node;

      final retrievedUser = User.fromNode(userNode);
      final retrievedPost = BlogPost.fromNode(postNode);

      print('User: ${retrievedUser.name} (ElementID: ${retrievedUser.elementId.elementIdOrThrow})');
      print('Post: ${retrievedPost.title} (ElementID: ${retrievedPost.elementId.elementIdOrThrow})');
    }

  } finally {
    await session.close();
    await driver.close();
  }
}

Requirements #

  • Dart SDK: >=3.0.0
  • Compatible with both regular Dart classes and Freezed classes
  • Requires build_runner for code generation

Contributing #

Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository.

License #

This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.

0
likes
0
points
15
downloads

Publisher

verified publisherex3.dev

Weekly Downloads

Annotations for Neo4j Object-Graph Mapping (OGM) code generation in Dart.

Repository (GitHub)
View/report issues

License

unknown (license)

More

Packages that depend on dart_neo4j_ogm