Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
393db57
feat(backend): Phase A — schema re-sync from web + v0.7.0 connector w…
Jul 2, 2026
383111c
fix(backend): resolve all schema-drift runtime breakage from the re-sync
Jul 2, 2026
a8c4f0b
feat(backend): Phase B (tranche 1) — retire JQB in domain + payout repos
Jul 2, 2026
5a8b7c6
chore(mobile): consume prisma_flutter_connector ^0.7.0 from pub.dev
Jul 2, 2026
2bd5ab0
chore(backend): re-sync schema to Jul-19 web source (129 models) + co…
Jul 23, 2026
1966956
feat(backend): retire JQB in all routes, route handlers, and services
Jul 23, 2026
fde0e4f
feat(backend): retire JQB + raw helpers across all repositories (bar …
Jul 23, 2026
753056d
feat(backend): appointment repository finale — 85 JQB sites → typed (…
Jul 23, 2026
756dd82
Merge origin/dev: schema-sync PR #120 + payment-free booking into typ…
Jul 23, 2026
b509eb9
feat(backend): connector ^0.9.0 — zero JsonQueryBuilder, gate at 0/0
Jul 23, 2026
13194c0
fix(stream): batch + dedupe user upserts — stop tripping Stream's Upd…
Jul 24, 2026
1d1505e
fix(backend): PR review — encrypt PAN, link OAuth profiles, idempoten…
Jul 24, 2026
a2f7dfc
fix(backend): PR review — validate enum inputs, ISO8601 scheduledAt, …
Jul 24, 2026
4c18c6e
fix(backend): PR review — guard client-input enum mappings (400 not 500)
Jul 24, 2026
3e312dd
style(backend): dart format pass (map line-wrapping from earlier tran…
Jul 24, 2026
5b4d3f3
fix(backend): PR review — professional-background PUT returns 400 on …
Jul 24, 2026
67aaa88
fix(backend): PR review round 2 — cancel 400, PAN decrypt logging, re…
Jul 24, 2026
f8c8a57
fix(backend): restore null-on-missing contract for nullable update me…
Jul 24, 2026
6589e74
test(backend): repair unit tests for the typed-delegate surface (user…
Jul 24, 2026
f0cfc32
test(backend): migrate account/verification/session/support-ticket te…
Jul 24, 2026
3221de2
test(backend): green suite — migrate explore scaffold, gate un-migrat…
Jul 24, 2026
24ad9d6
test(backend): migrate appointment + checkout suites; fix BigInt and …
Jul 24, 2026
1048862
test(backend): migrate the last 3 suites — full test suite green (307…
Jul 24, 2026
6833bfe
ci(backend): add Backend CI and fix the dead branch triggers
Jul 24, 2026
200e5be
ci(backend): resolve deps via the Flutter SDK
Jul 24, 2026
ca060ee
ci: bump the stale Flutter pin and correct the declared SDK floors
Jul 24, 2026
b8b7919
Revert the SDK-floor bump: it triggers Dart 3.7's new formatter
Jul 24, 2026
5486bfe
ci(app): stop the Flutter analyzer walking into backend/ and build/
Jul 24, 2026
4d00806
style(app): clear the mechanical lint debt exposed by re-enabling CI
Jul 24, 2026
d074bd1
fix(backend): PR review round 3 — real warnings the CI gate was hiding
Jul 25, 2026
d082409
fix(app): clear the last 10 lint issues; use action major tags not SHAs
Jul 25, 2026
2c69f1f
style(app): brace the wrapped if in auth_repository_impl
Jul 25, 2026
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
95 changes: 95 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Backend CI

on:
push:
branches: [dev, main, develop]
paths:
- 'backend/**'
- '.github/workflows/backend-ci.yml'
pull_request:
branches: [dev, main, develop]
paths:
- 'backend/**'
- '.github/workflows/backend-ci.yml'

# Read-only by default; no job here needs to write to the repo.
permissions:
contents: read

# Supersede in-flight runs for the same ref instead of queueing them.
concurrency:
group: backend-ci-${{ github.ref }}
cancel-in-progress: true

env:
# Kept in step with flutter-ci.yml's FLUTTER_VERSION. Must ship Dart >=3.7
# (bcrypt requires it) — the old 3.24.3 pin shipped Dart 3.5.3 and failed
# dependency resolution.
FLUTTER_VERSION: '3.44.1'

defaults:
run:
working-directory: backend

jobs:
backend:
name: Analyze, Format, Test
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Don't leave GITHUB_TOKEN in .git/config for later steps to read.
persist-credentials: false

# The backend is server-side Dart, but prisma_flutter_connector depends on
# the Flutter SDK, so pub resolution needs Flutter (not plain Dart).
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# lib/generated/** is gitignored — it is derived from prisma/schema.prisma
# (copied verbatim from familiarise_web) and must be regenerated here.
- name: Regenerate Prisma client + freezed
run: ./scripts/regenerate-build.sh --prisma

# `dart analyze` exits non-zero on *any* issue, including ~1k pre-existing
# style infos, so gate on severity instead. Parse the JSON report rather
# than the human-readable output: the text format labels these findings
# inconsistently (it showed 0 warnings where JSON showed 76), so a
# grep-based gate silently passed real warnings.
- name: Analyze (fail on errors + warnings)
run: |
dart analyze --format=json lib routes test > analyze.json || true
python3 - <<'EOF'
import json, sys, collections
with open('analyze.json') as f:
diagnostics = json.load(f)['diagnostics']
bad = [d for d in diagnostics if d['severity'] in ('ERROR', 'WARNING')]
print('diagnostics by severity:',
dict(collections.Counter(d['severity'] for d in diagnostics)))
for d in bad:
loc = d['location']
print(f"::error file={loc['file']},"
f"line={loc['range']['start']['line']}::"
f"{d['severity']} {d['code']}: {d['problemMessage']}")
if bad:
sys.exit(f"{len(bad)} error(s)/warning(s) found")
print('No errors or warnings.')
EOF

# git ls-files lists tracked files only, so the gitignored generated
# client is excluded for free.
- name: Check formatting
run: dart format --output=none --set-exit-if-changed $(git ls-files '*.dart')

- name: Test
run: dart test test/

# Ratchet: the JQB → typed-delegate migration finished at 0/0. This fails
# the build if raw JsonQueryBuilder or the removed raw finders creep back.
- name: JQB gate (no raw query builders)
run: ./scripts/jqb-gate.sh
11 changes: 8 additions & 3 deletions .github/workflows/flutter-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ name: Flutter CI/CD

on:
push:
branches: [main, develop]
# `dev` is this repo's default/base branch — without it this workflow
# never ran on day-to-day pushes or PRs.
branches: [dev, main, develop]
pull_request:
branches: [main, develop]
branches: [dev, main, develop]
release:
types: [published]

env:
FLUTTER_VERSION: '3.24.3'
# Must ship Dart >=3.7: bcrypt and skeletonizer both require it. The old
# 3.24.3 pin (Dart 3.5.3) failed resolution the moment this workflow
# actually ran again.
FLUTTER_VERSION: '3.44.1'
JAVA_VERSION: '17'

jobs:
Expand Down
11 changes: 11 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

analyzer:
exclude:
# `backend/` is a separate Dart package (dart_frog server) with its own
# pubspec, analysis_options and CI job. Its dependencies are not resolved
# in the Flutter workspace and its Prisma client is generated at build
# time, so analyzing it from here only produces phantom errors.
- backend/**
# Gitignored build output, including third-party SPM/CocoaPods checkouts
# (e.g. build/ios/SourcePackages/**) that carry their own example apps.
- build/**

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
Expand Down
11 changes: 10 additions & 1 deletion backend/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
include: package:dart_frog_lint/recommended.yaml
include: package:dart_frog_lint/recommended.yaml

analyzer:
exclude:
# Generated from prisma/schema.prisma by prisma_flutter_connector, plus the
# freezed/json_serializable output. Gitignored and never hand-edited, so it
# is not worth linting (and it dominates the issue count otherwise).
- lib/generated/**
- "**/*.freezed.dart"
- "**/*.g.dart"
121 changes: 57 additions & 64 deletions backend/lib/database/database_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@
// final user = await db.users.findByEmail(email);
// // OR type-safe: await db.prisma.feedback.create(data: ...);
//
// WHEN TO UPDATE THE SCHEMA REGISTRY
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Never by hand. Edit backend/prisma/schema.prisma (copied from the web
// repo, which owns migrations) and run:
// ./scripts/regenerate-build.sh --prisma
// WHEN THE SCHEMA CHANGES
// ~~~~~~~~~~~~~~~~~~~~~~~~
// Never by hand. Copy the source-of-truth schema from familiarise_web (which
// owns migrations) and regenerate — registry, models, and delegates are all
// derived automatically:
// 1. cp ../familiarise_web/prisma/schema.prisma prisma/schema.prisma
// 2. ./scripts/regenerate-build.sh --prisma
// (The old hand-maintained buildSchemaRegistry() in schema_registry_builder.dart
// is deprecated and no longer wired in.)
//
// If you add a model and need a repository:
// 1. Create a repository in backend/lib/database/repositories/
Expand Down Expand Up @@ -80,39 +84,41 @@ export '../generated/index.dart';
/// - QueryExecutor for query execution
/// - JsonQueryBuilder for type-safe query building
class DatabaseClient {
DatabaseClient._(this._executor, this._adapter, this._schema) {
DatabaseClient._(this._executor, this._adapter) {
// Initialize type-safe PrismaClient
_prisma = PrismaClient(adapter: _adapter);

// Initialize repositories
_userRepository = UserRepository(_executor, _prisma);
_accountRepository = AccountRepository(_executor, _prisma);
_sessionRepository = SessionRepository(_executor, _userRepository, _prisma);
_consulteeProfileRepository = ConsulteeProfileRepository(_executor, _prisma);
_consultantProfileRepository = ConsultantProfileRepository(_executor, _prisma);
_consulteeProfileRepository =
ConsulteeProfileRepository(_executor, _prisma);
_consultantProfileRepository =
ConsultantProfileRepository(_executor, _prisma);
_domainRepository = DomainRepository(_executor, _prisma);
_consultantExploreRepository = ConsultantExploreRepository(_executor);
_slotRepository = SlotRepository(_executor);
_appointmentRepository = AppointmentRepository(_executor);
_programsRepository = ProgramsRepository(_executor);
_checkoutRepository = CheckoutRepository(_executor);
_consultantExploreRepository =
ConsultantExploreRepository(_executor, _prisma);
_slotRepository = SlotRepository(_executor, _prisma);
_appointmentRepository = AppointmentRepository(_executor, _prisma);
_programsRepository = ProgramsRepository(_executor, _prisma);
_checkoutRepository = CheckoutRepository(_executor, _prisma);
_webhookEventRepository = WebhookEventRepository(_executor, _prisma);
_refundRepository = RefundRepository(_executor, _prisma);
_disputeRepository = DisputeRepository(_executor, _prisma);
_supportTicketRepository = SupportTicketRepository(_executor, _prisma);
_reviewRepository = ReviewRepository(_executor, _prisma);
_feedbackRepository = FeedbackRepository(_executor, _prisma);
_meetingSessionRepository = MeetingSessionRepository(_executor, _prisma);
_dashboardRepository = DashboardRepository(_executor);
_dashboardRepository = DashboardRepository(_executor, _prisma);
_verificationRepository = VerificationRepository(_executor, _prisma);
_collaboratorRepository = CollaboratorRepository(_executor, _prisma);
_referralRepository = ReferralRepository(_executor, _prisma);
_consultantVerificationRepository =
ConsultantVerificationRepository(_executor, _prisma);
_trialRepository = TrialRepository(_executor, _prisma);
_waitlistRepository = WaitlistRepository(_executor, _prisma);
_payoutAccountRepository =
PayoutAccountRepository(_executor, _prisma);
_payoutAccountRepository = PayoutAccountRepository(_executor, _prisma);
_appointmentDocumentRepository =
AppointmentDocumentRepository(_executor, _prisma);
_announcementRepository = AnnouncementRepository(_executor, _prisma);
Expand All @@ -125,7 +131,6 @@ class DatabaseClient {
static DatabaseClient? _instance;
final QueryExecutor _executor;
final PostgresAdapter _adapter;
final SchemaRegistry _schema;

// Type-safe PrismaClient (use this for new code)
late final PrismaClient _prisma;
Expand Down Expand Up @@ -153,13 +158,11 @@ class DatabaseClient {
late final VerificationRepository _verificationRepository;
late final CollaboratorRepository _collaboratorRepository;
late final ReferralRepository _referralRepository;
late final ConsultantVerificationRepository
_consultantVerificationRepository;
late final ConsultantVerificationRepository _consultantVerificationRepository;
late final TrialRepository _trialRepository;
late final WaitlistRepository _waitlistRepository;
late final PayoutAccountRepository _payoutAccountRepository;
late final AppointmentDocumentRepository
_appointmentDocumentRepository;
late final AppointmentDocumentRepository _appointmentDocumentRepository;
late final AnnouncementRepository _announcementRepository;
late final MaintenanceRepository _maintenanceRepository;
late final RecordingRepository _recordingRepository;
Expand All @@ -168,25 +171,6 @@ class DatabaseClient {

/// Build the schema registry from the generated registrations.
///
/// Models with @@map are additionally registered under their TABLE name
/// (e.g. both 'User' and 'users') so legacy JsonQueryBuilder calls that
/// reference .model('users') keep full field/relation metadata.
static SchemaRegistry _buildSchema() {
final schema = SchemaRegistry();
registerAllModels(schema);
for (final modelName in schema.modelNames.toList()) {
final model = schema.getModel(modelName);
if (model != null && model.tableName != model.name) {
schema.registerModel(ModelSchema(
name: model.tableName,
tableName: model.tableName,
fields: model.fields,
relations: model.relations,
));
}
}
return schema;
}

/// Initialize the database client with a connection URL
static Future<DatabaseClient> initialize(String connectionUrl) async {
Expand All @@ -202,37 +186,47 @@ class DatabaseClient {
colonIndex == -1 ? userInfo : userInfo.substring(0, colonIndex);
final password = colonIndex == -1 ? '' : userInfo.substring(colonIndex + 1);

// Honour ?sslmode=disable for local development databases; hosted
// Postgres (Supabase et al.) keeps the SSL requirement.
final sslMode = uri.queryParameters['sslmode'] == 'disable'
// Honour ?sslmode=disable, and auto-disable for localhost (no TLS on
// local Postgres); hosted Postgres (Supabase et al.) keeps SSL required.
final isLocal = uri.host == 'localhost' || uri.host == '127.0.0.1';
final sslMode = (uri.queryParameters['sslmode'] == 'disable' || isLocal)
? pg.SslMode.disable
: pg.SslMode.require;

final connection = await pg.Connection.open(
pg.Endpoint(
host: uri.host,
port: uri.port,
database:
uri.pathSegments.isNotEmpty ? uri.pathSegments.first : 'postgres',
username: username,
password: password,
// Pooled adapter (connector 0.7+): non-transactional statements run on
// connections borrowed from the pool; each transaction pins one dedicated
// connection. Replaces the previous single long-lived pg.Connection, whose
// silent staleness caused recurring 500s until a server restart.
final pool = pg.Pool<void>.withEndpoints(
[
pg.Endpoint(
host: uri.host,
port: uri.port,
database:
uri.pathSegments.isNotEmpty ? uri.pathSegments.first : 'postgres',
username: username,
password: password,
),
],
settings: pg.PoolSettings(
sslMode: sslMode,
maxConnectionCount: 8,
// Recycle pooled connections before hosted poolers kill them silently.
maxConnectionAge: const Duration(minutes: 30),
),
settings: pg.ConnectionSettings(sslMode: sslMode),
);

final adapter = PostgresAdapter(connection);
final schema = _buildSchema();
final adapter = PostgresAdapter.pooled(pool);

// Populate global registry so PrismaClient delegates can resolve
// @@map table names (e.g., 'User' → 'users' table).
for (final modelName in schema.modelNames) {
final model = schema.getModel(modelName);
if (model != null) schemaRegistry.registerModel(model);
}
// Populate the global registry from the GENERATED schema (all models,
// @@map/@map-aware, regenerated from prisma/schema.prisma). This replaces
// the hand-maintained buildSchemaRegistry() so JQB and typed PrismaClient
// delegates always match the current schema without manual upkeep.
registerAllModels(schemaRegistry);

final executor = QueryExecutor(adapter: adapter, schema: schema);
final executor = QueryExecutor(adapter: adapter, schema: schemaRegistry);

_instance = DatabaseClient._(executor, adapter, schema);
_instance = DatabaseClient._(executor, adapter);
return _instance!;
}

Expand Down Expand Up @@ -333,8 +327,7 @@ class DatabaseClient {
WaitlistRepository get waitlists => _waitlistRepository;

/// Payout account repository (for consultant bank/UPI accounts)
PayoutAccountRepository get payoutAccounts =>
_payoutAccountRepository;
PayoutAccountRepository get payoutAccounts => _payoutAccountRepository;

/// Appointment document repository (for document review workflow)
AppointmentDocumentRepository get appointmentDocuments =>
Expand Down
Loading
Loading