Как заставить клиентское приложение Firestore поддерживать правильное количество документов для коллекции? - PullRequest
1 голос
/ 27 января 2020

В Firestore нет способа запрашивать размер коллекции, поэтому, если клиентское приложение должно знать это значение, ему требуется некоторое обслуживание документа в другой коллекции для хранения этого количества. Однако требуется, чтобы клиент правильно выполнял транзакцию, чтобы поддерживать актуальность этого счета при добавлении и удалении документов. Злонамеренный или сломанный клиент может изменить либо коллекцию, либо счетчик независимо и создать ситуацию, когда счет не точен.

Возможно заставить клиентов через серверную часть выполнить это или использовать облачные функции триггер для автоматического поддержания счета (который вступит в силу после некоторой задержки). Однако я не хочу вводить бэкэнд, и я бы предпочел использовать правила безопасности. Как я могу это сделать?

Ответы [ 2 ]

3 голосов
/ 27 января 2020

Представьте, что у вас есть коллекция "сообщений", которая содержит сообщения, которые клиенты могут добавлять и удалять. Также представьте документ в другой коллекции с путем «messages-stats / data» с полем «count», которое поддерживает точное количество документов в сообщениях. Если клиентское приложение выполняет транзакцию, подобную этой, чтобы добавить документ:

async function addDocumentTransaction() {
    try {
        const ref = firestore.collection("messages").doc()
        const statsRef = firestore.collection("messages-stats").doc("data")
        await firestore.runTransaction(transaction => {
            transaction.set(ref, {
                foo: "bar"
            })
            transaction.update(statsRef, {
                count: firebase.firestore.FieldValue.increment(1),
                messageId: ref.id
            })
            return Promise.resolve()
        })
        console.log(`Added message ${ref.id}`)
    }
    catch (error) {
        console.error(error)
    }
}

Или пакет, подобный этому:

async function addDocumentBatch() {
    try {
        const batch = firestore.batch()
        const ref = firestore.collection("messages").doc()
        const statsRef = firestore.collection("messages-stats").doc("data")
        batch.set(ref, {
            foo: "bar"
        })
        batch.update(statsRef, {
            count: firebase.firestore.FieldValue.increment(1),
            messageId: ref.id
        })
        await batch.commit()
        console.log(`Added message ${ref.id}`)
    }
    catch (error) {
        console.error(error)
    }
}

И вот так, чтобы удалить документ с помощью транзакции:

async function deleteDocumentTransaction(id) {
    try {
        const ref = firestore.collection("messages").doc(id)
        const statsRef = firestore.collection("messages-stats").doc("data")
        await firestore.runTransaction(transaction => {
            transaction.delete(ref)
            transaction.update(statsRef, {
                count: firebase.firestore.FieldValue.increment(-1),
                messageId: ref.id
            })
            return Promise.resolve()
        })
        console.log(`Deleted message ${ref.id}`)
    }
    catch (error) {
        console.error(error)
    }
}

Или вот так с пакетом:

async function deleteDocumentBatch(id) {
    try {
        const batch = firestore.batch()
        const ref = firestore.collection("messages").doc(id)
        const statsRef = firestore.collection("messages-stats").doc("data")
        batch.delete(ref)
        batch.update(statsRef, {
            count: firebase.firestore.FieldValue.increment(-1),
            messageId: ref.id
        })
        await batch.commit()
        console.log(`Deleted message ${ref.id}`)
    }
    catch (error) {
        console.error(error)
    }
}

Тогда вы можете использовать правила безопасности, чтобы требовать, чтобы добавляемый или удаляемый документ мог быть изменен только одновременно с документ с полем подсчета. Минимально:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    match /messages/{id} {
      allow read;
      allow create: if
        getAfter(/databases/$(database)/documents/messages-stats/data).data.count ==
             get(/databases/$(database)/documents/messages-stats/data).data.count + 1;
      allow delete: if
        getAfter(/databases/$(database)/documents/messages-stats/data).data.count ==
             get(/databases/$(database)/documents/messages-stats/data).data.count - 1;
    }

    match /messages-stats/data {
      allow read;
      allow update: if (
        request.resource.data.count == resource.data.count + 1 &&
        existsAfter(/databases/$(database)/documents/messages/$(request.resource.data.messageId)) &&
           ! exists(/databases/$(database)/documents/messages/$(request.resource.data.messageId))
      ) || (
        request.resource.data.count == resource.data.count - 1 &&
        ! existsAfter(/databases/$(database)/documents/messages/$(request.resource.data.messageId)) &&
               exists(/databases/$(database)/documents/messages/$(request.resource.data.messageId))
      );
    }

  }
}

Обратите внимание, что клиент должен:

  • Увеличивать или уменьшать счет в /messages-stats/data при добавлении или удалении документа.
  • Должен предоставить идентификатор документа, добавляемого или удаляемого в документе «data» в поле с именем messageId.
  • Для увеличения счетчика необходимо, чтобы новый документ, указанный в messageId, не существовал до партии / транзакции фиксирует и существует после транзакции.
  • Для уменьшения счетчика требуется, чтобы старый документ, указанный в messageId, существовал до фиксации пакета / транзакции и не существовал после транзакции.

Обратите внимание, что existAfter () проверяет состояние названного документа после завершения транзакции , в то время как exist () проверяет его раньше. Разница между этими двумя функциями важна для того, как работают эти правила.

Также обратите внимание, что это не будет хорошо масштабироваться при большой нагрузке. Если документы добавляются и удаляются быстрее, чем 10 в секунду, для документа данных будет превышена скорость записи для каждого документа, и транзакция завершится неудачей.

После того, как это будет сделано, теперь вы можете фактически напишите правила безопасности, чтобы ограничить размер коллекции следующим образом:

match /messages/{id} {
  allow create: if
    get(/databases/$(database)/documents/messages-stats/data).data.count < 5;
}
0 голосов
/ 12 марта 2020

Предложенное решение все равно будет неудачным, так как вы можете просто добавить дополнительные документы в пакетную запись.

Но вы можете просто добавить в правило сбора сообщений следующее:

&& get(/databases/$(database)/documents/messages-stats/data).data.messageId == request.resource.id;

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    match /messages/{id} {
      allow read;
      allow create: if
        getAfter(/databases/$(database)/documents/messages-stats/data).data.count ==
             get(/databases/$(database)/documents/messages-stats/data).data.count + 1 
             && get(/databases/$(database)/documents/messages-stats/data).data.messageId == request.resource.id;
      allow delete: if
        getAfter(/databases/$(database)/documents/messages-stats/data).data.count ==
             get(/databases/$(database)/documents/messages-stats/data).data.count - 1
             && get(/databases/$(database)/documents/messages-stats/data).data.messageId == request.resource.id;
    }

    match /messages-stats/data {
      allow read;
      allow update: if (
        request.resource.data.count == resource.data.count + 1 &&
        existsAfter(/databases/$(database)/documents/messages/$(request.resource.data.messageId)) &&
           ! exists(/databases/$(database)/documents/messages/$(request.resource.data.messageId))
      ) || (
        request.resource.data.count == resource.data.count - 1 &&
        ! existsAfter(/databases/$(database)/documents/messages/$(request.resource.data.messageId)) &&
               exists(/databases/$(database)/documents/messages/$(request.resource.data.messageId))
      );
    }

  }
}
...