Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/mongodb-integration.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
name: MongoDB Integration Tests

# Disabled on push/PR until the MongoDB adapter is implemented
# (see note in ci.yml). Run manually via workflow_dispatch when needed.
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
workflow_call:

Expand Down
6 changes: 2 additions & 4 deletions .github/workflows/mysql-integration.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
name: MySQL Integration Tests

# Disabled on push/PR until the MySQL adapter is implemented
# (see note in ci.yml). Run manually via workflow_dispatch when needed.
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
workflow_call:

Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/supabase-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ jobs:

steps:
- name: Check if Supabase secrets are configured
id: check
run: |
if [[ -z "${{ secrets.SUPABASE_URL }}" ]] || \
[[ -z "${{ secrets.SUPABASE_ANON_KEY }}" ]] || \
[[ -z "${{ secrets.SUPABASE_DATABASE_URL }}" ]] || \
[[ -z "${{ secrets.SUPABASE_DIRECT_URL }}" ]]; then
echo "should-run=false" >> "$GITHUB_OUTPUT"
echo "⚠️ Supabase secrets not configured"
echo ""
echo "To run Supabase tests, configure these repository secrets:"
Expand All @@ -50,28 +52,34 @@ jobs:
exit 0
fi
fi
echo "should-run=true" >> "$GITHUB_OUTPUT"
echo "✓ All Supabase secrets found - proceeding with tests"

- name: Checkout code
if: steps.check.outputs.should-run == 'true'
uses: actions/checkout@v4
with:
submodules: 'recursive'

- name: Setup Node.js
if: steps.check.outputs.should-run == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Setup Flutter
if: steps.check.outputs.should-run == 'true'
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: 'stable'

- name: Install Prisma CLI
if: steps.check.outputs.should-run == 'true'
run: npm install -g prisma@5

- name: Setup environment variables
if: steps.check.outputs.should-run == 'true'
working-directory: test/integration/supabase
run: |
cat > .env << EOF
Expand All @@ -83,34 +91,39 @@ jobs:
EOF

- name: Run database migrations
if: steps.check.outputs.should-run == 'true'
working-directory: test/integration/supabase
run: prisma migrate deploy
env:
SUPABASE_DATABASE_URL: ${{ secrets.SUPABASE_DATABASE_URL }}
SUPABASE_DIRECT_URL: ${{ secrets.SUPABASE_DIRECT_URL }}

- name: Generate Prisma Client
if: steps.check.outputs.should-run == 'true'
working-directory: test/integration/supabase
run: prisma generate
env:
SUPABASE_DATABASE_URL: ${{ secrets.SUPABASE_DATABASE_URL }}
SUPABASE_DIRECT_URL: ${{ secrets.SUPABASE_DIRECT_URL }}

- name: Get Flutter dependencies
if: steps.check.outputs.should-run == 'true'
run: flutter pub get

- name: Generate Dart code from Prisma schema
if: steps.check.outputs.should-run == 'true'
run: |
dart run prisma_flutter_connector:generate \
--schema test/integration/supabase/schema.prisma \
--output test/integration/supabase/generated/

- name: Run Supabase integration tests
if: steps.check.outputs.should-run == 'true'
run: flutter test test/integration/supabase/supabase_test.dart --coverage

- name: Upload coverage
uses: codecov/codecov-action@v3
if: success()
if: success() && steps.check.outputs.should-run == 'true'
with:
files: ./coverage/lcov.info
flags: supabase-integration
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@ All notable changes to the Prisma Flutter Connector.

## [Unreleased]

## [0.6.0] - 2026-06-12

### Added

#### Parser: `@map` / `@@map` support
- **Model-level `@@map("table_name")`** is now parsed into `PrismaModel.dbName`, so generated delegates and the schema registry target the mapped database table (e.g., `model User { ... @@map("users") }` → `FROM "users"`). Explicit `@@map` takes precedence over reserved-keyword renames.
- **Field-level `@map("column_name")`** is now parsed into `PrismaField.dbName`, flowing into generated `@JsonKey` annotations, JSON serialization keys, and schema-registry column names (e.g., `status AppointmentStatus @map("requestStatus")`). Priority: explicit `@map` > reserved-keyword rename > PascalCase normalization.

#### SqlCompiler: field → column translation for `@map`-ed fields
- **WHERE keys, INSERT columns, UPDATE SET keys, and ORDER BY keys now resolve Dart field names to database column names** via the schema registry (`where: {'status': ...}` compiles to `"requestStatus" = $1` when the field carries `@map("requestStatus")`). This makes typed-delegate CRUD correct on mapped columns end-to-end — generated Create/Update/Where inputs emit Dart field names, which the compiler now maps.
- **Pass-through fallback preserved**: keys that are not registered field names (legacy JsonQueryBuilder callers using literal column names) compile unchanged, including inside `AND`/`OR`/`NOT` recursion.

#### SqlCompiler: `@updatedAt` auto-fill
- **`create`/`createMany` now fill `@updatedAt` columns** (NOW() on PostgreSQL/Supabase, ISO-8601 parameter elsewhere) — previously every typed-delegate create on a table with `updatedAt DateTime @updatedAt` failed with a NOT NULL violation.
- **`update`/`updateMany` refresh `@updatedAt`** unless the caller supplied a value (Prisma semantics). New `FieldInfo.isUpdatedAt` flag, emitted by the registry generator.

#### PostgresAdapter: enum[] / custom array decoding
- **Custom enum array columns (e.g. `SessionType[]`) now decode to `List<String?>`** instead of raw PostgreSQL wire-format bytes. Handles both the binary ARRAY wire format and text array literals (`{A,B,"c d",NULL}`), with NULL elements preserved.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
#### Registry generator: one-to-one FK on the target model
- **Relations whose foreign key lives on the TARGET model** (e.g. `Program.licensedSeatConfig` where `LicensedSeatConfig.programId` owns the `@relation`) are now emitted as `isOwner: false` with the target's real FK, instead of fabricating a nonexistent `<fieldName>Id` column on the parent — fixes `column tN.id does not exist` on nested includes.

### Fixed

#### Parser: enum block attributes treated as values
- **`@@map("...")` inside an enum body is no longer emitted as an enum value** (previously generated invalid Dart identifiers and broke compilation for schemas using mapped enums, e.g. BetterAuth/Prisma 7 schemas).
- **Value-level attributes on enum values are stripped** — `ACTIVE @map("active")` now parses as `ACTIVE`.

#### Delegate generator: models without unique scalar fields
- **Models whose only identifier is a composite `@@id([a, b])`** (no field-level `@id`/`@unique`) no longer generate delegates referencing a nonexistent `WhereUniqueInput` class. `findUnique`, `findUniqueOrThrow`, `update`, and `delete` are omitted for such models; `findFirst`, `findMany`, `updateMany`, `deleteMany`, `create`, and `count` remain available.

## [0.5.5] - 2026-04-04

### Fixed
Expand Down
19 changes: 12 additions & 7 deletions lib/src/generator/cb_delegate_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@ class CbDelegateGenerator {
Directive.import('../models/${toSnakeCase(modelName)}.dart'),
Directive.import('../filters.dart'),
])
..body.add(_buildDelegateClass(modelName, tableName)));
..body.add(_buildDelegateClass(modelName, tableName,
hasUniqueFields: model.fields
.any((f) => (f.isId || f.isUnique) && !f.isRelation))));

final emitter = DartEmitter(useNullSafetySyntax: true);
return _formatter.format('${library.accept(emitter)}');
}

Class _buildDelegateClass(String modelName, String tableName) {
Class _buildDelegateClass(String modelName, String tableName,
{required bool hasUniqueFields}) {
return Class((b) => b
..name = '${modelName}Delegate'
..docs.addAll([
Expand All @@ -53,22 +56,24 @@ class CbDelegateGenerator {
..name = '_executor'
..toThis = true))))
..methods.addAll([
_findUnique(modelName, tableName),
_findUniqueOrThrow(modelName),
// Models without any unique scalar field (e.g. composite @@id only)
// have no WhereUniqueInput, so unique-keyed methods are omitted
if (hasUniqueFields) _findUnique(modelName, tableName),
if (hasUniqueFields) _findUniqueOrThrow(modelName),
_findFirst(modelName, tableName),
_findMany(modelName, tableName),
_findManyRaw(modelName, tableName),
_findFirstRaw(modelName, tableName),
_create(modelName, tableName),
_createMany(modelName, tableName),
_update(modelName, tableName),
if (hasUniqueFields) _update(modelName, tableName),
_updateMany(modelName, tableName),
_delete(modelName, tableName),
if (hasUniqueFields) _delete(modelName, tableName),
_deleteMany(modelName, tableName),
_count(modelName, tableName),
_groupBy(modelName, tableName),
_normalizeForJson(),
_whereUniqueToJson(modelName),
if (hasUniqueFields) _whereUniqueToJson(modelName),
_whereToJson(modelName),
_orderByToJson(modelName),
]));
Expand Down
28 changes: 23 additions & 5 deletions lib/src/generator/cb_model_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,18 @@ class CbModelGenerator {
: "json['$key'] != null ? _\$${enumName}FromJson(json['$key'] as String) : null";
}

// BigInt cannot use @Default (no const constructor), so fields with a
// literal default are required in the Dart class and fromJson supplies
// the fallback. BigInt.parse on a string literal is precision-safe on
// all platforms (BigInt.from would round through a JS double on web).
if (dartType == 'BigInt') {
final parse = "BigInt.parse(json['$key'].toString())";
if (hasDefault) {
return "json['$key'] != null ? $parse : BigInt.parse('${f.defaultValue}')";
}
return f.isRequired ? parse : "json['$key'] != null ? $parse : null";
}
Comment thread
teetangh marked this conversation as resolved.

final defaultSuffix = hasDefault ? ' ?? ${f.defaultValue}' : '';
return switch (dartType) {
'String' => effectiveRequired
Expand All @@ -154,9 +166,6 @@ class CbModelGenerator {
'DateTime' => effectiveRequired
? "json['$key'] is DateTime ? json['$key'] as DateTime : DateTime.parse(json['$key'] as String)"
: "json['$key'] != null ? (json['$key'] is DateTime ? json['$key'] as DateTime : DateTime.parse(json['$key'] as String)) : null",
'BigInt' => effectiveRequired
? "BigInt.parse(json['$key'].toString())"
: "json['$key'] != null ? BigInt.parse(json['$key'].toString()) : null",
'Map<String, dynamic>' => f.isRequired
? "json['$key'] as Map<String, dynamic>"
: "json['$key'] as Map<String, dynamic>?",
Expand Down Expand Up @@ -341,7 +350,12 @@ class CbModelGenerator {
final hasScalarDefault = f.defaultValue != null &&
!f.isRelation &&
!_isPrismaRuntimeDefault(f.defaultValue!);
if (hasScalarDefault) {
if (hasScalarDefault && dartType == 'BigInt') {
// BigInt has no const constructor → @Default is impossible; the
// fromJson fallback (BigInt.from) supplies the schema default
type = dartType;
isRequired = true;
} else if (hasScalarDefault) {
annotations.add(CodeExpression(Code('Default(${f.defaultValue})')));
type = f.isList ? 'List<$dartType>' : dartType;
} else if (f.isRequired && !f.isList) {
Expand Down Expand Up @@ -402,7 +416,11 @@ class CbModelGenerator {
isRequired = true;
} else if (f.defaultValue != null &&
!_isPrismaRuntimeDefault(f.defaultValue!)) {
annotations.add(CodeExpression(Code('Default(${f.defaultValue})')));
if (dartType != 'BigInt') {
// BigInt has no const constructor → no @Default; leave the field
// nullable and let the database apply the schema default
annotations.add(CodeExpression(Code('Default(${f.defaultValue})')));
}
type = f.isList ? 'List<$dartType>?' : '$dartType?';
} else {
type = f.isList ? 'List<$dartType>?' : '$dartType?';
Expand Down
Loading
Loading