万年素人からHackerへの道

万年素人がHackerになれるまで殴り書きするぜ。

Firebase Data Connect -ユーザーログインなしでやる方法

以下のコマンドで初期化

firebase init dataconnect

GraphQLのファイル群

dataconnect/
├── dataconnect.yaml
├── default-connector
│   ├── connector.yaml
│   ├── mutations.gql
│   └── queries.gql
└── schema
    └── schema.gql

gqlコメントアウトを外す

VSCodeプラグインを入れてから保存するとdartのファイルができる

'generated/default_connector.dart';

Userをログインなしで一覧取得、追加、更新には以下が必要だった。

queries.gql

query ListUsers @auth(level: PUBLIC) {
  users {
    uid
    name
    address
  }
}

mutations.gql

# mutation CreateUser($name: String!, $address: String!) @auth(level: USER) {
mutation CreateUser($name: String!, $address: String!) @auth(level: PUBLIC) {
# mutation UpdateUser($uid: String!, $name: String!, $address: String!) @auth(level: USER) {
mutation UpdateUser($uid: String!, $name: String!, $address: String!) @auth(level: PUBLIC) {

connector.yaml

connectorId: "default-connector"
authMode: "PUBLIC" 
## ## Here's an example of how to add generated SDKs.
## ## You'll need to replace the outputDirs with ones pointing to where you want the generated code in your app.
# generate:
#   javascriptSdk:
#     outputDir: <Path where you want the generated SDK to be written to, relative to this file>
#     package: "@firebasegen/my-connector"
#     packageJSONDir: < Optional. Path to your Javascript app's package.json>
#   swiftSdk:
#     outputDir: <Path where you want the generated SDK to be written to, relative to this file>
#   kotlinSdk:
#     outputDir: <Path where you want the generated SDK to be written to, relative to this file>

generate:
  dartSdk:
    outputDir: "../../lib/generated"  # 出力先をFlutterプロジェクトに合わせて調整
    package: "com.example.yourproject"  # あなたのパッケージ名に置き換え(なくても動くが推奨)

編集終わったら、デプロイ

firebase deploy --only dataconnect

試行錯誤した main.dart

import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';
// 自動生成されたファイル
import 'generated/default_connector.dart';

late final FirebaseDataConnect dataConnect;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // Firebase App Check API をGCPで有効にしないとダメ
  // AppCheck をデバッグモードで有効にする(iOS/Android両対応)
  await FirebaseAppCheck.instance.activate(
    appleProvider: AppleProvider.debug,
  );

  await FirebaseAuth.instance.signInAnonymously();
  print('UID: ${FirebaseAuth.instance.currentUser?.uid}');

  final connectorConfig = DefaultConnectorConnector.connectorConfig;
  dataConnect =
      FirebaseDataConnect.instanceFor(connectorConfig: connectorConfig);

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'User CRUD',
      theme: ThemeData(useMaterial3: true),
      home: const UserScreen(),
    );
  }
}

class UserScreen extends StatefulWidget {
  const UserScreen({super.key});

  @override
  State<UserScreen> createState() => _UserScreenState();
}

class _UserScreenState extends State<UserScreen> {
  final _nameController = TextEditingController();
  final _addressController = TextEditingController();

  List<ListUsersUsers> _users = [];
  String _status = '';

  @override
  void initState() {
    super.initState();
    _fetchUsers();
  }

  Future<void> _fetchUsers() async {
    try {
      final builder = ListUsersVariablesBuilder(dataConnect);
      final result = await builder.execute();

      final users = result.data.users ?? [];
      setState(() {
        _users = users;
        _status = '取得成功 (${users.length}件)';
      });
    } catch (e) {
      print(e);
      setState(() {
        _status = '取得エラー: $e';
      });
    }
  }

  Future<void> _createUser() async {
    final name = _nameController.text.trim();
    final address = _addressController.text.trim();

    if (name.isEmpty || address.isEmpty) return;

    try {
      final builder = CreateUserVariablesBuilder(
        dataConnect,
        name: name,
        address: address,
      );

      final result = await builder.execute();

      _nameController.clear();
      _addressController.clear();
      await _fetchUsers();

      setState(() => _status = '追加成功');
    } on DataConnectOperationError catch (e) {
      print('❌ DataConnectOperationError 発生');
      print('コード: ${e.code}');
      print('メッセージ: ${e.message}');

      // エラー詳細を列挙
      for (final error in e.response.errors) {
        print('🚨 エラー: ${error.message}');
        print('🔍 パス: ${error.path.map((p) => p.toString()).join(" > ")}');
      }

      // 最初のエラーを UI に反映
      final firstMessage = e.response.errors.isNotEmpty
          ? e.response.errors.first.message
          : '不明なエラー';

      setState(() => _status = '追加失敗: $firstMessage');
    } catch (e, stack) {
      print('❗️ 例外発生: ${e.runtimeType}');
      print('内容: $e');
      print('StackTrace: $stack');
      setState(() => _status = '追加失敗(例外): $e');
    }
  }

  Future<void> _upsertUser() async {
    final name = _nameController.text.trim();
    final address = _addressController.text.trim();
    final uid = FirebaseAuth.instance.currentUser?.uid;
    if (uid == null) {
      print('❌ UIDが取得できませんでした');
      return;
    }

    try {
      // 1. すでに登録されているか確認
      final result = await ListUsersVariablesBuilder(dataConnect).execute();
      final user = result.data.users.firstWhere(
        (u) => u.uid == uid,
        // orElse: () => null,
      );

      if (user == null) {
        // 2. 登録されていなければ INSERT
        print('🆕 ユーザーが見つからないので新規登録します');
        await CreateUserVariablesBuilder(
          dataConnect,
          name: name,
          address: address,
        ).execute();
      } else {
        // 3. すでにあるなら UPDATE
        print('🔄 既存ユーザーを更新します');
        await UpdateUserVariablesBuilder(
          dataConnect,
          uid: uid,
          name: name,
          address: address,
        ).execute();
      }

      print('✅ 処理完了');
      await _fetchUsers();
    } catch (e) {
      print('❌ 例外: $e');
    }
  }

  // Future<void> _deleteMyUser() async {
  //   final uid = FirebaseAuth.instance.currentUser?.uid;
  //   if (uid == null) return;
  //
  //   final builder = UserDeleteVariablesBuilder(dataConnect);
  //   final result = await builder.execute(uid: uid);
  //
  //   if (result.hasError) {
  //     setState(() => _status = '削除失敗: ${result.error}');
  //   } else {
  //     _fetchUsers();
  //     setState(() => _status = '削除成功');
  //   }
  // }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ユーザー一覧 / 追加 / 削除')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
                controller: _nameController,
                decoration: const InputDecoration(labelText: '名前')),
            TextField(
                controller: _addressController,
                decoration: const InputDecoration(labelText: '住所')),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(onPressed: _upsertUser, child: const Text('追加')),
                // ElevatedButton(
                //     onPressed: _deleteMyUser, child: const Text('自分を削除')),
              ],
            ),
            const SizedBox(height: 16),
            Text('ステータス: $_status'),
            const Divider(),
            const Text('ユーザー一覧', style: TextStyle(fontWeight: FontWeight.bold)),
            Expanded(
              child: ListView.builder(
                itemCount: _users.length,
                itemBuilder: (_, i) {
                  final user = _users[i];
                  return ListTile(
                    title: Text(user.name ?? 'No Name'),
                    subtitle: Text(user.address ?? ''),
                  );
                },
              ),
            )
          ],
        ),
      ),
    );
  }
}

Firebase App Check APIGCPで有効にしないとダメだった あとは、 のAppCheckから

での、デバッグトークンを管理

以下をして、

Xcode でプロジェクトを開く(ios/Runner.xcworkspace)

メニューから Product → Scheme → Edit Scheme… を選ぶ

左の「Run」を選択

上部タブから「Arguments」を開く

Arguments Passed on Launch に → -FIRDebugEnabled を追加

「Close」で保存して完了!

Xcode から実行 flutter run や Android Studio からの起動では -FIRDebugEnabled は有効になりません

Xcodeで以下のようなログが出る。

Firebase App Check Debug Token: xxxxxxx-xx-xx-xxx-xxxx

名前をテキトーにつけて、Schemeの編集で保存する。