Skip to content

teetangh/prisma-flutter-connector

Repository files navigation

Prisma Flutter Connector

pub package License: MIT Tests

A type-safe, database-agnostic Flutter connector that generates type-safe Dart clients from your Prisma schema. Build Flutter apps with seamless Prisma ORM integration, automatic code generation, and support for multiple databases.

Features

  • Database Agnostic - PostgreSQL, MySQL, MongoDB, SQLite, Supabase
  • Type-Safe - End-to-end type safety from database to UI
  • Code Generation - Auto-generate Dart models and APIs from Prisma schema
  • GraphQL-Based - Efficient queries with Pothos + Apollo Server
  • Freezed Models - Immutable models with JSON serialization
  • Real-time - WebSocket subscriptions for live updates
  • Error Handling - Typed exceptions for better debugging
  • Developer-Friendly - Intuitive API inspired by Prisma Client
  • CI/CD Ready - GitHub Actions workflows included

Supported Databases

  • PostgreSQL - Full support with UUID, JSON, arrays
  • MySQL - Full support with auto-increment, decimals
  • MongoDB - Full support with ObjectId, embedded documents
  • SQLite - Full support with file-based storage
  • Supabase - Full support with PostgreSQL + Auth integration

Table of Contents

Installation

Add to your pubspec.yaml:

dependencies:
  prisma_flutter_connector: ^0.2.8

Run:

flutter pub get

Server Usage (Dart Frog, Shelf, etc.)

For pure Dart server environments, use the server-specific import to avoid Flutter dependencies:

// In your Dart server (Dart Frog, Shelf, etc.)
import 'package:prisma_flutter_connector/runtime_server.dart';

// For Flutter apps (includes SQLite support)
import 'package:prisma_flutter_connector/runtime.dart';

The runtime_server.dart exports only PostgreSQL and Supabase adapters, which use the pure Dart postgres package. This avoids the dart:ui not available error that occurs when the SQLite adapter (which depends on Flutter's sqflite) is imported in server environments.

Quick Start

1. Create Your Prisma Schema

// schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Product {
  id          String   @id @default(cuid())
  name        String
  description String
  price       Float
  stock       Int      @default(0)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

2. Generate Dart Code

# Generate type-safe Dart models and API clients
dart run prisma_flutter_connector:generate \
  --schema prisma/schema.prisma \
  --output lib/generated/

This creates:

  • lib/generated/models/ - Freezed models (Product, CreateProductInput, etc.)
  • lib/generated/api/ - Type-safe API clients (ProductsAPI)
  • lib/generated/client.dart - PrismaClient with all APIs

3. Initialize the Client

import 'package:prisma_flutter_connector/prisma_flutter_connector.dart';
import 'generated/client.dart';

final client = PrismaClient(
  config: PrismaConfig(
    graphqlEndpoint: 'http://localhost:4000/graphql',
    debugMode: true,
  ),
);

4. Use Type-Safe APIs

// List all products
final products = await client.products.list();

// Filter products
final cheapProducts = await client.products.list(
  filter: ProductFilter(priceUnder: 100),
);

// Create product
final newProduct = await client.products.create(
  input: CreateProductInput(
    name: 'Laptop',
    description: 'Gaming laptop',
    price: 1999.99,
    stock: 10,
  ),
);

// Update product
final updated = await client.products.update(
  id: newProduct.id,
  input: UpdateProductInput(stock: 5),
);

Code Generation

The connector works by generating type-safe Dart code from your Prisma schema. This ensures complete type safety from your database to your Flutter UI.

Generator CLI

dart run prisma_flutter_connector:generate [options]

Options:

  • --schema - Path to your Prisma schema file (required)
  • --output - Output directory for generated code (required)

Example:

dart run prisma_flutter_connector:generate \
  --schema prisma/schema.prisma \
  --output lib/generated/

What Gets Generated

For each model in your Prisma schema, the generator creates:

  1. Freezed Model (models/product.dart)

    @freezed
    class Product with _$Product {
      const factory Product({
        required String id,
        required String name,
        required String description,
        required double price,
        required int stock,
        required DateTime createdAt,
        required DateTime updatedAt,
      }) = _Product;
    
      factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
    }
  2. Input Types (models/product_input.dart)

    @freezed
    class CreateProductInput with _$CreateProductInput {
      const factory CreateProductInput({
        required String name,
        required String description,
        required double price,
        int? stock,
      }) = _CreateProductInput;
    }
    
    @freezed
    class UpdateProductInput with _$UpdateProductInput {
      const factory UpdateProductInput({
        String? name,
        String? description,
        double? price,
        int? stock,
      }) = _UpdateProductInput;
    }
  3. Filter Types (models/product_filter.dart)

    @freezed
    class ProductFilter with _$ProductFilter {
      const factory ProductFilter({
        String? nameContains,
        double? priceUnder,
        double? priceOver,
        bool? inStock,
      }) = _ProductFilter;
    }
  4. API Client (api/products_api.dart)

    class ProductsAPI extends BaseAPI<Product> {
      Future<Product?> findUnique({required String id});
      Future<List<Product>> list({ProductFilter? filter, ProductOrderBy? orderBy});
      Future<Product> create({required CreateProductInput input});
      Future<Product> update({required String id, required UpdateProductInput input});
      Future<Product> delete({required String id});
    }
  5. PrismaClient (client.dart)

    class PrismaClient extends BasePrismaClient {
      late final ProductsAPI products;
      // ... other model APIs
    }

Running Build Runner

After code generation, run build_runner to generate Freezed and JSON serialization code:

flutter pub run build_runner build --delete-conflicting-outputs

Workflow

# 1. Define your Prisma schema
vim prisma/schema.prisma

# 2. Generate Prisma Client (Node.js)
npx prisma generate

# 3. Generate Dart code
dart run prisma_flutter_connector:generate \
  --schema prisma/schema.prisma \
  --output lib/generated/

# 4. Generate Freezed code
flutter pub run build_runner build --delete-conflicting-outputs

# 5. Use in your app
# Import generated client and start querying!

Usage

Queries

// List with filters
final products = await client.products.list(
  filter: ProductFilter(
    nameContains: 'laptop',
    priceUnder: 2000,
    inStock: true,
  ),
);

// Find unique
final product = await client.products.findUnique(id: 'prod-123');

// Get order with relations
final order = await client.orders.findUnique(id: 'order-123');
print('Order Total: \$${order?.total}');
print('Items: ${order?.items?.length}');

Mutations

// Create user
final user = await client.users.create(
  input: CreateUserInput(
    email: 'alice@example.com',
    name: 'Alice Johnson',
  ),
);

// Create order
final order = await client.orders.create(
  input: CreateOrderInput(
    userId: user.id,
    items: [
      OrderItemInput(productId: 'prod-1', quantity: 2),
    ],
  ),
);

// Update order status
final updated = await client.orders.update(
  id: order.id,
  input: UpdateOrderInput(status: OrderStatus.shipped),
);

// Delete product
final deleted = await client.products.delete(id: 'product-id');

Subscriptions

// Subscribe to order creation
final subscription = client.orders.subscribeToOrderCreated(
  userId: 'user-123',
);

subscription.listen(
  (order) => print('New order: ${order.id}'),
  onError: (error) => print('Error: $error'),
);

// Subscribe to order status changes
final statusSub = client.orders.subscribeToOrderStatusChanged(
  orderId: 'order-123',
);

statusSub.listen((order) {
  print('Status: ${order.status}');
});

Error Handling

try {
  final product = await client.products.findUnique(id: 'invalid-id');
} on NotFoundException catch (e) {
  print('Not found: ${e.message}');
} on ValidationException catch (e) {
  print('Validation error: ${e.message}');
  e.fieldErrors?.forEach((field, errors) {
    print('  $field: ${errors.join(", ")}');
  });
} on NetworkException catch (e) {
  print('Network error: ${e.message}');
} on PrismaException catch (e) {
  print('Error: ${e.message}');
}

Advanced Features

Computed Fields (Correlated Subqueries)

Add inline aggregations to your queries using correlated subqueries:

import 'package:prisma_flutter_connector/runtime.dart';

final query = JsonQueryBuilder()
    .model('Product')
    .action(QueryAction.findMany)
    .computed({
      'minVariantPrice': ComputedField.min('price',
        from: 'ProductVariant',
        where: {'productId': FieldRef('id')}),
      'maxVariantPrice': ComputedField.max('price',
        from: 'ProductVariant',
        where: {'productId': FieldRef('id')}),
      'reviewCount': ComputedField.count(
        from: 'Review',
        where: {'productId': FieldRef('id')}),
      'topCategory': ComputedField.first('name',
        from: 'Category',
        where: {'productId': FieldRef('id')},
        orderBy: {'priority': 'asc'}),
    })
    .where({'isActive': true})
    .orderBy({'rating': 'desc'})
    .build();

// Generates SQL like:
// SELECT "t0".*,
//   (SELECT MIN("price") FROM "ProductVariant" WHERE "productId" = "t0"."id") AS "minVariantPrice",
//   (SELECT MAX("price") FROM "ProductVariant" WHERE "productId" = "t0"."id") AS "maxVariantPrice",
//   (SELECT COUNT(*) FROM "Review" WHERE "productId" = "t0"."id") AS "reviewCount",
//   (SELECT "name" FROM "Category" WHERE "productId" = "t0"."id" ORDER BY "priority" ASC LIMIT 1) AS "topCategory"
// FROM "Product" "t0"
// WHERE "isActive" = $1
// ORDER BY "rating" DESC

Available Computed Field Types:

  • ComputedField.min(field, from:, where:) - MIN aggregate
  • ComputedField.max(field, from:, where:) - MAX aggregate
  • ComputedField.avg(field, from:, where:) - AVG aggregate
  • ComputedField.sum(field, from:, where:) - SUM aggregate
  • ComputedField.count(from:, where:, field:) - COUNT aggregate (optional field for COUNT(field))
  • ComputedField.first(field, from:, where:, orderBy:) - First matching value

Select Specific Fields

Reduce network overhead by selecting only the columns you need:

final query = JsonQueryBuilder()
    .model('Product')
    .action(QueryAction.findMany)
    .selectFields(['id', 'name', 'price', 'rating'])
    .where({'isActive': true})
    .orderBy({'price': 'asc'})
    .take(10)
    .build();

// Generates: SELECT "id", "name", "price", "rating" FROM "Product"
//            WHERE "isActive" = $1 ORDER BY "price" ASC LIMIT 10

Relation Filtering

Filter records based on related data using EXISTS subqueries:

final query = JsonQueryBuilder()
    .model('Product')
    .action(QueryAction.findMany)
    .where({
      'isActive': true,
      // Products with at least one review rated >= 4
      'reviews': FilterOperators.some({
        'rating': FilterOperators.gte(4),
      }),
      // Products in the electronics category
      'categories': FilterOperators.some({
        'id': 'category-electronics',
      }),
    })
    .orderBy({'price': {'sort': 'asc', 'nulls': 'last'}})
    .build();

Relation Filter Operators:

  • FilterOperators.some(where) - At least one related record matches
  • FilterOperators.every(where) - All related records match
  • FilterOperators.noneMatch(where) - No related records match
  • FilterOperators.isEmpty() - Relation has no records
  • FilterOperators.isNotEmpty() - Relation has at least one record

FILTER Clause for Aggregations (PostgreSQL/Supabase)

Get conditional counts in a single query:

final statsQuery = JsonQueryBuilder()
    .model('Review')
    .action(QueryAction.aggregate)
    .aggregation({
      '_count': true,
      '_avg': {'rating': true},
      '_countFiltered': [
        {'alias': 'fiveStar', 'filter': {'rating': 5}},
        {'alias': 'fourStar', 'filter': {'rating': 4}},
        {'alias': 'threeStar', 'filter': {'rating': 3}},
      ],
    })
    .where({'productId': 'product-123'})
    .build();

// Generates: SELECT COUNT(*) AS "_count", AVG("rating") AS "_avg_rating",
//   COUNT(*) FILTER (WHERE "rating" = $1) AS "fiveStar",
//   COUNT(*) FILTER (WHERE "rating" = $2) AS "fourStar", ...

Case-Insensitive Search

Search with case-insensitive matching (uses ILIKE on PostgreSQL):

final query = JsonQueryBuilder()
    .model('User')
    .action(QueryAction.findMany)
    .where({
      'name': FilterOperators.containsInsensitive('john'),  // Matches "John", "JOHN", "john"
    })
    .build();

Backend Setup

This package requires a GraphQL backend using Prisma + Pothos + Apollo Server. See the backend example for complete implementation.

Quick Backend Setup

cd backend_example
npm install
cp .env.example .env
npm run prisma:generate
npm run prisma:migrate
npm run prisma:seed
npm run dev

The GraphQL API will be available at http://localhost:4000/graphql.

Prisma Schema Example

model Product {
  id          String   @id @default(cuid())
  name        String
  description String
  price       Float
  stock       Int      @default(0)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

See backend_example/prisma/schema.prisma for the complete E-commerce schema.

Database Setup

The connector supports multiple databases. Choose the one that fits your needs:

PostgreSQL

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String
  posts     Post[]
  createdAt DateTime @default(now())
}

Connection String:

DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

Docker:

docker run -d \
  -e POSTGRES_PASSWORD=password \
  -p 5432:5432 \
  postgres:16-alpine

MySQL

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Product {
  id       Int      @id @default(autoincrement())
  name     String
  price    Decimal  @db.Decimal(10, 2)
  category Category @relation(fields: [categoryId], references: [id])
}

Connection String:

DATABASE_URL="mysql://user:password@localhost:3306/mydb"

Docker:

docker run -d \
  -e MYSQL_ROOT_PASSWORD=password \
  -e MYSQL_DATABASE=mydb \
  -p 3306:3306 \
  mysql:8.0

MongoDB

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}

model Blog {
  id       String   @id @default(auto()) @map("_id") @db.ObjectId
  title    String
  content  String
  tags     String[]
  metadata Json?
}

Connection String:

DATABASE_URL="mongodb://localhost:27017/mydb"

Docker:

docker run -d \
  -p 27017:27017 \
  mongo:7.0

SQLite

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model Task {
  id          String   @id @default(cuid())
  title       String
  completed   Boolean  @default(false)
  priority    Int      @default(0)
}

No installation needed - SQLite is file-based!

Supabase

datasource db {
  provider  = "postgresql"
  url       = env("SUPABASE_DATABASE_URL")
  directUrl = env("SUPABASE_DIRECT_URL")
}

model Profile {
  id        String   @id @default(uuid())
  userId    String   @unique  // Supabase Auth user ID
  username  String   @unique
  avatarUrl String?
}

Connection Strings:

# Session pooler (Transaction mode) - for queries
SUPABASE_DATABASE_URL="postgresql://postgres.your-project:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres"

# Direct connection (Session mode) - required for migrations
SUPABASE_DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.your-project.supabase.co:5432/postgres"

Why two URLs?

  • DATABASE_URL (pooled): Connection pooling for efficient queries
  • DIRECT_URL (direct): Required for Prisma migrations (bypasses pooler)

Get connection strings from Supabase project settings → Database → Connection string.

Running Migrations

# Create a migration
npx prisma migrate dev --name init

# Apply migrations (production)
npx prisma migrate deploy

# MongoDB: use db push instead
npx prisma db push

Architecture

High-Level Overview

Prisma Schema (.prisma)
    ↓
Code Generator (bin/generate.dart)
    ↓
├── Dart Models (Freezed)
├── API Clients (type-safe)
└── PrismaClient
    ↓
GraphQL Client (graphql_flutter)
    ↓
Backend (Apollo Server + Pothos)
    ↓
Prisma ORM
    ↓
Database (PostgreSQL/MySQL/MongoDB/SQLite/Supabase)

Components

1. Code Generator (lib/src/generator/)

  • PrismaParser: Parses .prisma schema files
  • ModelGenerator: Generates Freezed models and input types
  • APIGenerator: Generates type-safe API clients
  • CLI Tool: bin/generate.dart - Command-line interface

2. Runtime (lib/src/)

  • BasePrismaClient: Generic client that works with any schema
  • BaseAPI: Base class for generated API clients
  • PrismaConfig: Configuration (endpoints, auth, caching)
  • Exceptions: Typed error handling

3. Generated Code

  • Models: Freezed classes with JSON serialization
  • Input Types: CreateInput, UpdateInput for mutations
  • Filter Types: Type-safe query filters
  • API Clients: One per model (ProductsAPI, UsersAPI, etc.)
  • PrismaClient: Main entry point with all APIs

4. Backend Stack

  • GraphQL - Chosen over REST for mobile efficiency
  • Pothos GraphQL - Auto-generates schema from Prisma
  • Apollo Server - GraphQL server with subscriptions
  • Prisma ORM - Database ORM with migrations

Why Code Generation?

  1. Database Agnostic: Works with any Prisma-supported database
  2. Zero Hardcoding: No model-specific code in the library
  3. Type Safety: Full type safety from database to UI
  4. Developer Experience: One command to generate entire client
  5. Maintainability: Schema changes automatically propagate

See docs/ARCHITECTURE.md for detailed architecture.

Testing

The connector includes comprehensive tests across multiple databases with easy-to-use Makefile commands and test scripts.

Quick Start

# Run all tests (recommended)
make test-all

# Run unit tests only
make test-unit

# Run specific database tests
make test-postgres
make test-mysql
make test-mongodb
make test-sqlite

Test Structure

test/
├── unit/               # Unit tests for components
├── integration/        # Integration tests with real databases
│   ├── postgres/      # PostgreSQL tests
│   ├── mysql/         # MySQL tests
│   ├── mongodb/       # MongoDB tests
│   ├── sqlite/        # SQLite tests
│   └── supabase/      # Supabase tests
└── e2e/               # End-to-end tests

Using Makefile (Recommended)

# Testing
make test-all          # Run all tests (lint + unit + integration)
make test-unit         # Run unit tests
make test-integration  # Run all integration tests
make test-postgres     # PostgreSQL integration tests
make test-mysql        # MySQL integration tests
make test-mongodb      # MongoDB integration tests
make test-sqlite       # SQLite integration tests
make test-supabase     # Supabase integration tests (requires .env)

# Database Setup
make setup-postgres    # Start PostgreSQL container
make setup-mysql       # Start MySQL container
make setup-mongodb     # Start MongoDB container

# Cleanup
make cleanup-all       # Stop all containers

# Code Quality
make lint              # Format and analyze code
make format            # Format Dart code
make analyze           # Run Dart analyzer

Using Test Scripts

# Run entire test suite
./scripts/test-runner.sh

# Run only unit tests
./scripts/test-runner.sh --only-unit

# Run only integration tests
./scripts/test-runner.sh --only-integration

# Test specific database
./scripts/test-database.sh postgres
./scripts/test-database.sh mysql
./scripts/test-database.sh mongodb
./scripts/test-database.sh sqlite

See test/README.md for detailed testing instructions and manual setup.

GitHub Actions CI/CD

The project uses modular workflows for better maintainability:

Workflows:

  • .github/workflows/ci.yml - Master workflow (runs all tests)
  • .github/workflows/unit-tests.yml - Unit tests
  • .github/workflows/lint.yml - Code quality
  • .github/workflows/*-integration.yml - Individual database tests

Test Execution:

  1. Code quality checks (formatting, linting)
  2. Unit tests
  3. Integration tests (run in parallel):
    • PostgreSQL (GitHub Actions service)
    • MySQL (GitHub Actions service)
    • MongoDB (Docker Compose)
    • SQLite (file-based)
    • Supabase (requires secrets)

Triggering Workflows:

  • Automatically on push/PR to main or develop
  • Manually from GitHub Actions tab (any workflow)

View workflows in .github/workflows/

Contributing Tests

When adding features:

  1. Add unit tests for new components
  2. Add integration tests for database-specific features
  3. Run make test-all before submitting PR
  4. Ensure all CI checks pass
  5. Update test documentation if needed

See .github/CONTRIBUTING.md for contribution guidelines.

Documentation

Core Documentation

Testing & Contributing

Examples

Additional Resources

Roadmap

v0.1.0 (Current)

  • Code generation from Prisma schema
  • Multi-database support (PostgreSQL, MySQL, MongoDB, SQLite, Supabase)
  • Type-safe Freezed models
  • GraphQL-based API clients
  • Basic CRUD operations
  • Real-time subscriptions
  • Error handling
  • Comprehensive test suite
  • CI/CD with GitHub Actions

v0.2.0 (Planned)

  • Offline support with Drift
  • Cache-first queries
  • Optimistic UI updates
  • Pagination (cursor-based)
  • Aggregations (count, sum, avg)
  • Relations loading (include/select)

v0.3.0 (Future)

  • Batch operations
  • File uploads
  • Advanced filters (full-text search)
  • Transaction support
  • Query builder API
  • Middleware support

v1.0.0 (Future)

  • Production stability
  • Performance optimizations
  • Comprehensive documentation
  • Migration guides
  • VS Code extension
  • CLI improvements

Contributing

Contributions are welcome! Please read our Contributing Guide for details on:

  • Development setup
  • Running tests
  • Code style guidelines
  • Pull request process
  • Setting up GitHub secrets for CI/CD

Quick start:

  1. Fork the repository
  2. Clone and initialize submodules: git clone --recurse-submodules
  3. Install dependencies: flutter pub get
  4. Create a feature branch: git checkout -b feature/my-feature
  5. Make your changes and add tests
  6. Run tests: flutter test
  7. Run analyzer: flutter analyze
  8. Commit and push
  9. Open a Pull Request

License

MIT License - see LICENSE file for details.

Credits

Created with ❤️ for the Flutter and Prisma communities.

Special thanks to:

Support


Note: This is v0.1.0 - the initial release. Offline caching and advanced features are planned for future versions.

About

A type-safe, database-agnostic Flutter connector that generates type-safe Dart clients from your Prisma schema. Build Flutter apps with seamless Prisma ORM integration, automatic code generation, and support for multiple databases.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors