Özel çözümleyicilerle Data Connect'in kapsamını genişletme

Özel çözümleyiciler yazarak Firebase Data Connect'yı Cloud SQL'e ek olarak diğer veri kaynaklarını destekleyecek şekilde genişletebilirsiniz. Ardından, birden fazla veri kaynağını (Cloud SQL ve özel çözümleyicileriniz tarafından sağlanan veri kaynakları) tek bir sorguda veya mutasyonda birleştirebilirsiniz.

"Veri kaynağı" kavramı esnektir. E-kitap şunları içerir:

  • Cloud Firestore, MongoDB ve diğerleri gibi Cloud SQL dışındaki veritabanları.
  • Cloud Storage, AWS S3 gibi depolama hizmetleri
  • Stripe, SendGrid, Salesforce gibi API tabanlı tüm entegrasyonlar.
  • Özel iş mantığı.

Ek veri kaynaklarınızı desteklemek için özel çözümleyiciler yazdıktan sonra Data Connect sorgularınız ve mutasyonlarınız bunları birçok şekilde birleştirebilir. Örneğin:

  • Veri kaynaklarınız için birleştirilmiş yetkilendirme katmanı. Örneğin, Cloud SQL'de depolanan verileri kullanarak Cloud Storage'daki dosyalara erişimi yetkilendirin.
  • Web, Android ve iOS için tür güvenli istemci SDK'ları.
  • Birden fazla kaynaktan veri döndüren sorgular.
  • Veritabanı durumunuza göre işlev çağırmaları kısıtlanır.

Ön koşullar

Kendi özel çözümleyicilerinizi yazmak için aşağıdakilere ihtiyacınız vardır:

  • Firebase CLI v15.9.0 veya üstü
  • Firebase Functions SDK'sı v7.1.0 veya üzeri

Ayrıca, özel çözümleyicilerinizin mantığını uygulayacağınız Cloud Functions for Firebase'i kullanarak işlev yazma konusunda bilgi sahibi olmanız gerekir.

Başlamadan önce

Data Connect uygulamasını kullanmak için önceden bir proje oluşturmuş olmanız gerekir.

Henüz yapmadıysanız kurulum için aşağıdaki hızlı başlangıç kılavuzlarından birini uygulayabilirsiniz:

Özel çözümleyiciler yazma

Genel olarak, özel çözümleyici yazma işlemi üç bölümden oluşur: İlk olarak, özel çözümleyiciniz için bir şema tanımlama; ikinci olarak, çözümleyicilerinizi Cloud Functions kullanarak uygulama ve son olarak, özel çözümleyici alanlarınızı sorgularda ve mutasyonlarda kullanma (muhtemelen Cloud SQL veya diğer özel çözümleyicilerle birlikte).

Bunu nasıl yapacağınızı öğrenmek için sonraki birkaç bölümdeki adımları uygulayın. Motive edici bir örnek olarak, kullanıcılarınızın herkese açık profil bilgilerinin Cloud SQL dışında depolandığını varsayalım. Bu örneklerde tam olarak hangi veri deposunun kullanıldığı belirtilmemiştir ancak Cloud Storage, MongoDB örneği veya başka bir şey olabilir.

Aşağıdaki bölümlerde, bu harici profil bilgilerini Data Connect'ya aktarabilen özel bir çözümleyicinin temel uygulaması gösterilecektir.

Özel çözümleyicinizin şemasını tanımlama

  1. Firebase proje dizininizde şu komutu çalıştırın:

    firebase init dataconnect:resolver

    Firebase CLI, özel çözümleyiciniz için bir ad girmenizi ister ve TypeScript veya JavaScript'te örnek çözümleyici uygulamaları oluşturup oluşturmayacağınızı sorar. Bu kılavuzu takip ediyorsanız varsayılan adı kabul edin ve TypeScript örnekleri oluşturun.

    Araç daha sonra boş bir dataconnect/schema_resolver/schema.gql dosyası oluşturur ve yeni çözümleyici yapılandırmanızı dataconnect.yaml dosyasına ekler.

  2. Bu schema.gql dosyasını, özel çözümleyicinizin sağlayacağı sorguları ve mutasyonları tanımlayan bir GraphQL şemasıyla güncelleyin. Örneğin, Cloud SQL dışında bir veri deposunda depolanan, kullanıcının herkese açık profilini alıp güncelleyebilen özel bir çözümleyici için şema aşağıda verilmiştir:

    # dataconnect/schema_resolver/schema.gql
    
    type PublicProfile {
      name: String!
      photoUrl: String!
      bioLine: String!
    }
    
    type Query {
      # This field will be backed by your Cloud Function.
      publicProfile(userId: String!): PublicProfile
    }
    
    type Mutation {
      # This field will be backed by your Cloud Function.
      updatePublicProfile(
        userId: String!, name: String, photoUrl: String, bioLine: String
      ): PublicProfile
    }
    

Özel çözümleyici mantığını uygulama

Ardından, çözümleyicilerinizi Cloud Functions'ı kullanarak uygulayın. Arka planda bir GraphQL sunucusu oluşturacaksınız. Ancak Cloud Functions'ta bu işlemin ayrıntılarını ele alan bir yardımcı yöntem (onGraphRequest) bulunur. Bu nedenle, yalnızca veri kaynağınıza erişen çözümleyici mantığını yazmanız gerekir.

  1. Dosyayı açın functions/src/index.ts.

    Yukarıdaki firebase init dataconnect:resolver komutunu çalıştırdığınızda bu Cloud Functions kaynak kodu dizini oluşturulmuş ve index.ts içinde örnek kodla başlatılmıştır.

  2. Aşağıdaki tanımları ekleyin:

    import {
      FirebaseContext,
      onGraphRequest,
    } from "firebase-functions/dataconnect/graphql";
    
    const opts = {
      // Points to the schema you defined earlier, relative to the root of your
      // Firebase project.
      schemaFilePath: "dataconnect/schema_resolver/schema.gql",
      resolvers: {
        query: {
          // This resolver function populates the data for the "publicProfile" field
          // defined in your GraphQL schema located at schemaFilePath.
          publicProfile(
            _parent: unknown,
            args: Record<string, unknown>,
            _contextValue: FirebaseContext,
            _info: unknown
          ) {
            const userId = args.userId;
    
            // Here you would use the user ID to retrieve the user profile from your data
            // store. In this example, we just return a hard-coded value.
    
            return {
              name: "Ulysses von Userberg",
              photoUrl: "https://example.com/profiles/12345/photo.jpg",
              bioLine: "Just a guy on a mountain. Ski fanatic.",
            };
          },
        },
        mutation: {
          // This resolver function updates data for the "updatePublicProfile" field
          // defined in your GraphQL schema located at schemaFilePath.
          updatePublicProfile(
            _parent: unknown,
            args: Record<string, unknown>,
            _contextValue: FirebaseContext,
            _info: unknown
          ) {
            const { userId, name, photoUrl, bioLine } = args;
    
            // Here you would update in your datastore the user's profile using the
            // arguments that were passed. In this example, we just return the profile
            // as though the operation had been successful.
    
            return { name, photoUrl, bioLine };
          },
        },
      },
    };
    
    export const resolver = onGraphRequest(opts);
    

Bu iskelet uygulamalar, bir çözümleyici işlevinin alması gereken genel şekli gösterir. Tam işlevli bir özel çözümleyici oluşturmak için yorumlu bölümleri veri kaynağınıza okuma ve yazma işlemleri yapan kodlarla doldurmanız gerekir.

Sorgularda ve mutasyonlarda özel çözümleyicileri kullanma

Özel çözümleyicinizin şemasını tanımladığınıza ve destekleyen mantığı uyguladığınıza göre artık özel çözümleyiciyi Data Connect sorgularınızda ve mutasyonlarınızda kullanabilirsiniz. Daha sonra, Cloud SQL, özel çözümleyicileriniz veya bunların bir kombinasyonu tarafından desteklenen tüm verilerinize erişmek için kullanabileceğiniz özel bir istemci SDK'sını otomatik olarak oluşturmak üzere bu işlemleri kullanacaksınız.

  1. dataconnect/example/queries.gql alanına aşağıdaki tanımı ekleyin:

    query GetPublicProfile($id: String!)
        @auth(level: PUBLIC, insecureReason: "Anyone can see a public profile.") {
      publicProfile(userId: $id) {
        name
        photoUrl
        bioLine
      }
    }
    

    Bu sorgu, özel çözümleyicinizi kullanarak kullanıcının herkese açık profilini alır.

  2. dataconnect/example/mutations.gql bölümüne aşağıdaki tanımı ekleyin:

    mutation SetPublicProfile(
      $id: String!, $name: String, $photoUrl: String, $bioLine: String
    ) @auth(expr: "vars.id == auth.uid") {
      updatePublicProfile(userId: $id, name: $name, photoUrl: $photoUrl, bioLine: $bioLine) {
        name
        photoUrl
        bioLine
      }
    }
    

    Bu mutasyon, özel çözümleyicinizi kullanarak veri deposuna yeni bir profil verileri grubu yazar. Şemanın, kullanıcıların yalnızca kendi profillerini güncelleyebilmesini sağlamak için Data Connect'nın @auth yönergesini kullandığını unutmayın. Veri deponuza Data Connect üzerinden eriştiğiniz için bu gibi Data Connect özelliklerinden otomatik olarak yararlanabilirsiniz.

Yukarıdaki örneklerde, özel çözümleyicilerinizi kullanarak veri deponuzdaki verilere erişen Data Connect işlemleri tanımladınız. Ancak işlemlerinizde Cloud SQL'den veya tek bir özel veri kaynağından verilere erişme konusunda sınırlı değilsiniz. Birden fazla kaynaktan gelen verileri birleştiren daha gelişmiş kullanım alanları için Örnekler bölümüne bakın.

Özel çözümleyicilerinizi nasıl kullanacağınızı görmek için bir sonraki bölüme geçin.

Özel çözümleyicinizi ve işlemlerinizi dağıtma

Data Connect Şemalarınızda değişiklik yaptığınızda, değişikliklerin geçerli olması için şemaları dağıtmanız gerekir. Bunu yapmadan önce, Cloud Functions kullanarak uyguladığınız özel çözümleyici mantığını dağıtın:

firebase deploy --only functions

Artık güncellenen şemaları ve işlemleri dağıtabilirsiniz:

firebase deploy --only dataconnect

Data Connect şemalarınızda değişiklik yaptıktan sonra yeni istemci SDK'ları da oluşturmanız gerekir:

firebase dataconnect:sdk:generate

Örnekler

Bu örneklerde, daha gelişmiş bazı kullanım alanlarının nasıl uygulanacağı ve yaygın hatalardan nasıl kaçınılacağı gösterilmektedir.

Cloud SQL'deki verileri kullanarak özel bir çözümleyiciye erişimi yetkilendirme

Veri kaynaklarınızı özel çözümleyiciler kullanarak Data Connect entegre etmenin avantajlarından biri, veri kaynaklarını birleştiren işlemler yazabilmenizdir.

Bu örnekte, bir sosyal medya uygulaması oluşturduğunuzu ve bir süredir kullanıcıyla etkileşimde bulunmayan arkadaşına otomatik hatırlatma e-postası gönderen bir mutasyonu özel çözümleyici olarak uyguladığınızı varsayalım.

Otomatik hatırlatma özelliğini uygulamak için aşağıdaki gibi bir şemaya sahip özel bir çözümleyici oluşturun:

# A GraphQL server must define a root query type per the spec.
type Query {
  unused: String
}

type Mutation {
  sendEmail(id: String!, content: String): Boolean
}

Bu tanım, aşağıdakiler gibi bir Cloud Functions işleviyle desteklenir:

import {
  FirebaseContext,
  onGraphRequest,
} from "firebase-functions/dataconnect/graphql";

const opts = {
  schemaFilePath: "dataconnect/schema_resolver/schema.gql",
  resolvers: {
    mutation: {
      sendEmail(
        _parent: unknown,
        args: Record<string, unknown>,
        _contextValue: FirebaseContext,
        _info: unknown
      ) {
        const { id, content } = args;

        // Look up the friend's email address and call the cloud service of your
        // choice to send the friend an email with the given content.

       return true;
      },
    },
  },
};

export const resolver = onGraphRequest(opts);

E-posta göndermek hem sizin için maliyetli hem de kötüye kullanım için potansiyel bir vektör olduğundan, sendEmail özel çözümleyicinizi kullanmadan önce amaçlanan alıcının kullanıcının arkadaş listesinde olduğundan emin olmanız gerekir.

Uygulamanızda arkadaş listesi verilerinin Cloud SQL'de depolandığını varsayalım:

type User @table {
  id: String! @default(expr: "auth.uid")
  acceptNudges: Boolean! @default(value: false)
}

type UserFriend @table(key: ["user", "friend"]) {
  user: User!
  friend: User!
}

E-postayı göndermek için özel çözümleyiciyi kullanmadan önce gönderenin alıcının arkadaş listesinde olduğundan emin olmak üzere önce Cloud SQL'e sorgu gönderen bir mutasyon yazabilirsiniz:

# Send a "nudge" to a friend as a reminder. This will only let the user send a
# nudge if $friendId is in the user's friends list.
mutation SendNudge($friendId: String!) @auth(level: USER_EMAIL_VERIFIED) {
  # Step 1: Query and check
  query @redact {
    userFriend(
      key: {userId_expr: "auth.uid", friendId: $friendId}
    # This checks that $friendId is in the user's friends list.
    ) @check(expr: "this != null", message: "You must be friends to nudge") {
      friend {
        # This checks that the friend is accepting nudges.
        acceptNudges @check(expr: "this == true", message: "Not accepting nudges")
      }
    }
  }
  # Step 2: Act
  sendEmail(id: $friendId, content: "You've been nudged!")
}

Bu örnek, özel çözümleyiciler bağlamındaki bir veri kaynağının veritabanları ve benzeri sistemler dışındaki kaynakları da içerebileceğini gösterir. Bu örnekte, veri kaynağı bir bulut e-posta gönderme hizmetidir.

Mutasyonları kullanarak sıralı yürütmeyi sağlama

Veri kaynaklarını birleştirirken genellikle bir veri kaynağına yapılan isteğin tamamlanmasını beklemeniz ve ardından farklı bir veri kaynağına istek göndermeniz gerekir. Örneğin, yapay zeka API'si kullanarak isteğe bağlı bir videoyu dinamik olarak yazıya döken bir sorgunuz olduğunu varsayalım. Bunun gibi bir API çağrısı pahalı olabilir. Bu nedenle, kullanıcı videonun sahibi olmalı veya uygulamanızda bir tür premium kredi satın almış olmalı gibi bazı ölçütlerle çağrıyı sınırlamak isteyebilirsiniz.

Bunu elde etmeye yönelik ilk deneme şu şekilde görünebilir:

# This won't work as expected.
query BrokenTranscribeVideo($videoId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
  # Step 1: Check quota using SQL.
  # Verify the user owns the video and has "pro" status or credits.
  checkQuota: query @redact {
    video(id: $videoId)
    {
      user @check(expr: "this.id == auth.uid && this.hasCredits == true", message: "Unauthorized access") {
        id
        hasCredits
      }
    }
  }

  # Step 2: Trigger expensive compute
  # Only triggers if Step 1 succeeds? No! This won't work because query field
  # execution order is not guaranteed.
  triggerTranscription: query {
    # For example, might call Vertex AI or Transcoder API.
    startVideoTranscription(videoId: $videoId)
  }
}

Sorgu alanlarının yürütme sırası garanti edilmediği için bu yaklaşım işe yaramaz. GraphQL sunucusu, eşzamanlılığı en üst düzeye çıkarmak için alanları herhangi bir sırada çözümleyebilmeyi bekler. Öte yandan, GraphQL sunucusu bir mutasyonun bazı alanlarının çözümlendiğinde yan etkileri olabileceğini beklediğinden mutasyon alanları her zaman sırayla çözümlenir.

Örnek işlemin ilk adımında yan etkiler olmasa da mutasyon alanlarının sırayla çözümlenmesinden yararlanmak için işlemi mutasyon olarak tanımlayabilirsiniz:

# By using a mutation, we guarantee the SQL check happens FIRST.
mutation TranscribeVideo($videoId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
  # Step 1: Check quota using SQL.
  # Verify the user owns the video and has "pro" status or credits.
  checkQuota: query @redact {
    video(id: $videoId)
    {
      user @check(expr: "this.id == auth.uid && this.hasCredits == true", message: "Unauthorized access") {
        id
        hasCredits
      }
    }
  }

  # Step 2: Trigger expensive compute
  # This Cloud Function will ONLY trigger if Step 1 succeeds.
  triggerTranscription: query {
    # For example, might call Vertex AI or Transcoder API.
    startVideoTranscription(videoId: $videoId)
  }
}

Sınırlamalar

Özel çözümleyiciler özelliği, deneysel bir herkese açık önizleme olarak yayınlanmıştır. Şu anki sınırlamalar:

Özel çözümleyici bağımsız değişkenlerinde CEL ifadeleri yok

CEL ifadelerini özel bir çözümleyicinin bağımsız değişkenlerinde dinamik olarak kullanamazsınız. Örneğin, aşağıdakiler mümkün değildir:

mutation UpdateMyProfile($newName: String!) @auth(level: USER) {
  updateMongoDocument(
    collection: "profiles"
    # This isn't supported:
    id_expr: "auth.uid"
    update: { name: $newName }
  )
}

Bunun yerine, standart değişkenleri (ör. $authUid) iletin ve güvenli bir şekilde değerlendirilen @auth(expr: ...) yönergesini kullanarak bunları işlem düzeyinde doğrulayın.

mutation UpdateMyProfile(
  $newName: String!, $authUid: String!
) @auth(expr: "vars.authUid == auth.uid") {
  updateMongoDocument(
    collection: "profiles"
    id: $authUid
    update: { name: $newName }
  )
}

Diğer bir geçici çözüm ise tüm mantığınızı özel bir çözümleyiciye taşımak ve tüm veri işlemlerinizi Cloud Functions'tan tamamlamaktır.

Örneğin, şu anda çalışmayan şu örneği ele alalım:

mutation BrokenForwardToEmail($chatMessageId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
  query {
    chatMessage(id: $chatMessageId) {
      content
    }
  }
  sendEmail(
    title: "Forwarded Chat Message"
    to_expr: "auth.token.email" # Not supported.
    content_expr: "response.query.chatMessage.content" # Not supported.
  )
}

Bunun yerine, hem Cloud SQL sorgusunu hem de e-posta hizmetine yapılan çağrıyı bir işlevle desteklenen tek bir mutasyon alanına taşıyın:

mutation ForwardToEmail($chatMessageId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
  forwardChatToEmail(
    chatMessageId: $chatMessageId
  )
}

Veritabanınız için bir Admin SDK oluşturun ve Cloud SQL sorgusunu gerçekleştirmek üzere işlevde kullanın:

const opts = {
 schemaFilePath: "dataconnect/schema_resolver/schema.gql",
 resolvers: {
   query: {
     async forwardToEmail(
       _parent: unknown,
       args: Record<string, unknown>,
       _contextValue: FirebaseContext,
       _info: unknown
     ) {
       const chatMessageId = args.chatMessageId as string;

       let decodedToken;
       try {
         decodedToken = await getAuth().verifyIdToken(_contextValue.auth.token ?? "");
       } catch (error) {
         return false;
       }

       const email = decodedToken.email;
       if (!email) {
         return false;
       }

       const response = await getChatMessage({chatMessageId});
       const messageContent = response.data.chatMessage?.content;

       // Here you call the cloud service of your choice to send the email with
       // the message content.

       return true;
     }
   },
 },
};
export const resolver = onGraphRequest(opts);

Özel çözümleyici parametrelerinde giriş nesnesi türü yok

Özel çözümleyiciler, karmaşık GraphQL giriş türlerini kabul etmez. Parametreler temel skaler türler (String, Int, Date, Any vb.) ve Enum olmalıdır.

input PublicProfileInput {
  name: String!
  photoUrl: String!
  bioLine: String!
}

type Mutation {
  # Not supported:
  updatePublicProfile(userId: String!, profile: PublicProfileInput): PublicProfile

  # OK:
  updatePublicProfile(userId: String!, name: String, photoUrl: String, bioLine: String): PublicProfile
}

Özel çözümleyiciler, SQL işlemlerinden önce gelemez

Bir mutasyonda, özel bir çözümleyiciyi standart SQL işlemlerinden önce yerleştirmek hataya neden olur. Tüm SQL tabanlı işlemler, özel çözümleyici çağrılarından önce görünmelidir.

İşlem yok (@transaction)

Özel çözümleyiciler, standart SQL işlemleri içeren bir @transaction bloğuna sarmalanamaz. Çözücüyü destekleyen Cloud Function, bir SQL ekleme işlemi başarılı olduktan sonra başarısız olursa veritabanı otomatik olarak geri alınmaz.

SQL ile başka bir veri kaynağı arasında işlemsel güvenlik sağlamak için SQL işlem mantığını Cloud Function'ın içine taşıyın ve doğrulama ile geri alma işlemlerini Admin SDK'yı veya doğrudan SQL bağlantılarını kullanarak gerçekleştirin.