Привет, друзья!
В этом цикле из 2 статей я хочу рассказать вам о Supabase
— открытой (open source), т.е. бесплатной альтернативе Firebase
. Первая статья будет посвящена теории, во второй — мы вместе с вами разработаем полноценное social app с аутентификацией, базой данных, хранилищем файлов и обработкой изменения данных в режиме реального времени.
Что такое Supabase?
Supabase
, как и Firebase
— это SaaS
(software as a service — программное обеспечение как услуга) или BaaS
(backend as a service — бэкенд как услуга). Что это означает? Это означает, что при разработке fullstack app мы разрабатываем только клиентскую часть, а все остальное предоставляется Supabase
через пользовательские комплекты для разработки программного обеспечения (SDK) и интерфейсы прикладного программирования (API). Под «всем остальным» подразумевается сервис аутентификации (включая возможность использования сторонних провайдеров), база данных (PostgreSQL), файловое хранилище, realtime (реакцию на изменение данных в реальном времени), и сервер, который все это обслуживает.
Если вам это интересно, прошу под кат.
Лаконичность дальнейшего изложения обусловлена желанием уместить всю (ну почти) необходимую информацию в одной статье.
Поехали!
Установка
yarn add @supabase/supabase-js
Импорт
import supabase from '@supabase/supabase-js'
Клиент (Client)
Инициализация
import { createClient } from '@supabase/supabase-js' const supabase = createClient(url, key, options)
url
— путь, предоставляемый после создания проекта, доступный в настройках панели управления проектом;key
— ключ, предоставляемый после создания проекта, доступный в настройках панели управления проектом;options
— дополнительные настройки.
const options = { // дефолтная схема schema: 'public', headers: { 'x-my-custom-header': 'my-app-name' }, autoRefreshToken: true, persistSession: true, detectSessionInUrl: true } const supabase = createClient('https://my-app.supabase.co', 'public-anon-key', options)
По умолчанию для выполнения HTTP-запросов supabase-js
использует библиотеку cross-fetch
. Это можно изменить следующим образом:
const supabase = createClient('https://my-app.supabase.co', 'public-anon-key', { fetch: fetch.bind(globalThis) })
Аутентификация (Auth)
Регистрация
Для регистрации нового пользователя используется метод signUp
:
async function registerUser({ email, password, first_name, last_name, age }) { try { const { user, session, error } = await supabase.auth.signUp({ // обязательно email, password }, { // опционально data: { // такой синтаксис является валидным в JS // и более подходящим для Postgres first_name, last_name, age }, // дефолтная настройка redirectTo: window.location.origin }) if (error) throw error return { user, session } } catch (e) { throw e } }
Такая сигнатура позволяет вызывать данный метод следующим образом (на примере React-приложения
):
// предположим, что `registerUser` возвращает только пользователя const onSubmit = (e) => { e.preventDefault() if (submitDisabled) return setLoading(true) userApi .registerUser(formData) .then(setUser) .catch(setError) .finally(() => { setLoading(false) }) }
В TypeScript
можно использовать такую сигнатуру:
async function registerUser({ first_name, last_name, age, email, password }: UserData): ResponseData { let data = { user: null, error: null } try { const { user, error } = await supabase.auth.signUp({ email, password }, { data: { first_name, last_name, age } }) if (error) { data.error = error } else { data.user = user } } catch (e) { data.error = e } finally { return data } }
Обратите внимание: по умолчанию после регистрации пользователь должен подтвердить свой email. Это можно отключить в разделе Authentication -> Settings
.
Авторизация
Для авторизации, в том числе с помощью сторонних провайдеров, используется метод signIn
:
async function loginUser({ email, password }) { try { const { user, session, error } = await supabase.auth.singIn({ // обязательно email, password }, { // опционально returnTo: window.location.pathname }) if (error) throw error return { user, session } } catch (e) { throw e } }
Если авторизация выполняется только через email, пользователю отправляется так называемая магическая ссылка/magic link
.
Пример авторизации с помощью аккаунта GitHub
:
async function loginWithGitHub() { try { const { user, session, error } = await supabase.auth.signIn({ // провайдером может быть `google`, `github`, `gitlab` или `bitbucket` provider: 'github' }, { scopes: 'repo gist' }) if (error) throw error // для доступа к `API` провайдера используется `session.provider_token` return { user, session } } catch (e) { throw e } }
Выход из системы
Для выхода из системы используется метод signOut
:
async function logoutUser() { try { const { error } = await supabase.auth.signOut() if (error) throw error } catch (e) { throw e } }
Для того, чтобы сообщить серверу о необходимости завершения сессии используется метод auth.api.signOut(jwt)
.
Сессия
Метод session
используется для получения данных активной сессии:
const session = supabase.auth.session()
Пользователь
Метод user
возвращает данные авторизованного пользователя:
const user = supabase.auth.user()
В данном случае возвращается объект из локального хранилища.
Для получения данных пользователя из БД используется метод auth.api.getUser(jwt)
.
На стороне сервера для этого используется метод auth.api.getUserByCookie
.
Обновление данных пользователя
Для обновления данных пользователя используется метод update
:
async function updateUser({ email, age }) { try { const { user, error } = await supabase.auth.update({ email }, { data: { age } }) if (error) throw error return user } catch (e) { throw e } }
Регистрация изменения состояния авторизации
supabase.auth.onAuthStateChange((event, session) => { console.log(event, session) })
Сброс пароля
const { data, error } = supabase.auth.api.resetPasswordForEmail(email, { redirectTo: window.location.origin })
После вызова этого метода на email пользователя отправляется ссылка для сброса пароля. Когда пользователь кликает по ссылке, он переходит по адресу: SITE_URL/#access_token=X&refresh_token=Y&expires_in=Z&token_type=bearer&type=recovery
. Мы регистрируем type=recovery
и отображаем форму для сброса пароля. Затем используем access_token
из URL
и новый пароль для обновления пользовательских данных:
async function resetPassword(access_token, new_password) { try { const { data, error } = await supabase.auth.api.updateUser(access_token, { password: new_password }) if (error) throw error return data } catch (e) { throw e } }
База данных (Database)
Извлечение (выборка) данных
Сигнатура:
const { data, error } = await supabase .from(table_name) .select(column_names, options)
table_name
— название таблицы (обязательно);column_names
— список разделенных через запятую названий колонок (столбцов) или'*'
для выборки всех полей;options
— дополнительные настройки:
head
— если имеет значениеtrue
, данные аннулируются (см. пример ниже);count
— алгоритм для подсчета количества строк в таблице:null | exact | planned | estimated
.
Пример:
async function getUsers() { try { const { data, error } = await supabase .from('users') .select('id, first_name, last_name, age, email') if (error) throw error return data } catch (e) { throw(e) } }
По умолчанию максимальное количество возвращаемых строк равняется 1000
. Это можно изменить в настройках.
select()
может использоваться совместно с модификаторами и фильтрами (будут рассмотрены позже).
Получение данных из связанных таблиц
async function getUsersAndPosts() { try { const { data, error } = await supabase .from('users') .select(` id, first_name, last_name, age, email, posts ( id, title, content ) `) if (error) throw error return data } catch (e) { throw e } }
Фильтрация данных с помощью внутреннего соединения (inner join)
Предположим, что мы хотим извлечь сообщения (messages), принадлежащие пользователю с определенным именем (username):
async function getMessagesByUsername(username) { try { const { data, error } = await supabase .from('messages') // для такой фильтрации используется функция `!inner()` .select('*, users!inner(*)') .eq('users.username', username) if (error) throw error return data } catch (e) { throw e } }
Получение количества строк
async function getUsersAndUserCount() { try { const { data, error, count } = await supabase .from('users') // для получения только количества строк используется `{ count: 'exact', head: true }` .select('id, first_name, last_name, email', { count: 'exact' }) if (error) throw error return { data, count } } catch(e) { throw e } }
Получение данных из JSONB-столбцов
async function getUsersWithTheirCitiesByCountry(country) { try { const { data, error } = await supabase .from('users') .select(` id, first_name, last_name, email, address->city `) .eq('address->country', country) if (error) throw error return data } catch(e) { throw e } }
Получение данных в формате CSV
async function getUsersInCSV() { try { const { data, error } = await supabase .from('users') .select() .csv() if (error) throw error return data } catch(e) { throw e } }
Запись (добавление, вставка) данных
Сигнатура:
const { data, error } = await supabase .from(table_name) .insert(data_array, options)
table_name
— название таблицы;data_array
— массив записываемых данных;options
— дополнительные настройки, например:
returning: 'minimal'
— не возвращать данные после записи;upsert: true
— обновить существующую строку вместо создания новой.
Пример:
async function createPost({ title, body, author_id }) { try { const { data, error } = await supabase .from('posts') .insert([{ title, body, author_id }]) if (error) throw error return data } catch (e) { throw e } }
Разумеется, можно создавать несколько записей одновременно:
// предположим, что `messages` - массив объектов `{ title, body, user_id, room_id }` async function saveMessagesForRoom(messages) { try { const { error } = await supabase .from('messages') .insert(messages, { returning: 'minimal' }) if (error) throw error return data } catch (e) { throw e } }
Обратите внимание: для обновления строк с помощью upsert: true
данные должны содержать первичные или основные ключи/primary keys
соответствующей таблицы.
Модификация данных
Сигнатура:
const { data, error } = await supabase .from(table_name) .update({ column_name: 'column_value' }, options) .match(condition)
table_name
— название таблицы;column_name: 'column_value'
— название обновляемой колонки и ее новое значение;options
— дополнительные настройки:
returning
— определяет, возвращаются ли данные после обновления:minimal | presentation
;count
— алгоритм для подсчета количества обновленных строк;
condition
— условие для поиска обновляемой колонки.
Пример:
async function updateUser({ changes, user_id }) { try { const { data, error } = await supabase .from('users') .update({ ...changes }) .match({ id: user_id }) if (error) throw error return data } catch (e) { throw e } }
Обратите внимание: метод update
должен всегда использоваться совместно с фильтрами.
Пример обновления JSON-колонки
async function updateUsersCity({ address, user_id }) { try { const { error } = await supabase .from('users') .update(`address: ${address}`, { returning: 'minimal' }) .match({ id: user_id }) if (error) throw error } catch (e) { throw e } }
В настоящее время поддерживается обновление только объекта целиком.
Модификация или запись данных
Сигнатура:
const { data, error } = await supabase .from(table_name) .upsert({ primary_key_name: 'primary_key_value', column_name: 'column_value' }, options)
options
— дополнительные настройки:
returning
;count
;onConflict: string
— позволяет работать с колонками, имеющими ограничениеUNIQUE
;ignoreDuplicates: boolean
— определяет, должны ли игнорироваться дубликаты.
Пример:
async function updateOrCreatePost({ id, title, body, author_id }) { try { const { data, error } = await supabase .from('posts') .upsert({ id, title, body, author_id }) if (error) throw error return data } catch (e) { throw e } }
Разумеется, можно обновлять или создавать несколько записей за раз.
Обратите внимание на 2 вещи:
- данные должны содержать первичные ключи;
- первичные ключи могут быть только натуральными (не суррогатными).
Удаление данных
Сигнатура:
const { data, error } = await supabase .from(table_name) .delete(options) .match(condition)
options
:
returning
;count
.
Пример:
async function removeUser(user_id) { try { const { error } = await supabase .from('users') .delete({ returning: 'minimal' }) .match({ id: user_id }) if (error) throw error } catch (e) { throw e } }
Вызов функций Postgres
Сигнатура:
const { data, error } = await supabase .rpc(fn, params, options)
fn
— название функции;params
— параметры, передаваемые функции;options
:
head
;count
.
Предположим, что мы определили такую функцию:
CREATE FUNCTION check_password(username_or_email TEXT, password_hash TEXT) RETURNS BOOLEAN AS $$ DECLARE is_password_correct BOOLEAN; BEGIN SELECT (hashed_password = $2) INTO is_password_correct FROM users WHERE username = $1 OR email = $1; RETURN is_password_correct; END; $$ LANGUAGE plpgsql
Данную функцию можно вызвать следующим образом:
async function checkPassword(username_or_email, password) { let password_hash try { password_hash = await bcrypt.hash(password, 10) } catch (e) { throw e } try { const { data, error } = await supabase .rpc('check_password', { username_or_email, password_hash }) if (error) throw error return data } catch(e) { throw e } }
Метод rpc
может использоваться совместно с модификаторами и фильтрами, например:
const { data, error } = await supabase .rpc('get_all_cities') .select('city_name', 'population') .eq('city_name', 'The Shire')
Модификаторы (Modifiers)
Модификаторы используются в запросах на выборку данных (select
). Они также могут использоваться совместно с методом rpc
, когда он возвращает таблицу.
limit()
Модификатор limit
ограничивает количество возвращаемых строк.
Сигнатура:
const { data, error } = await supabase .from(table_name) .select(column_names) .limit(count, options)
count
: количество возвращаемых строк;options
:
foreignTable
: внешняя таблица (для колонок с внешним ключом).
Пример:
async function getTop10Users() { try { const { data, error } = await supabase .from('users') .select('id, user_name, email') .limit(10) if (error) throw error return data } catch(e) { throw e } }
Пример с внешней таблицей:
async function getTop10CitiesByCountry(country_name) { try { const { data, error } = await supabase .from('countries') .select('country_name, cities(city_name)') .eq('country_name', 'Rus') .limit(10, { foreignTable: 'cities' }) if (error) throw error return data } catch(e) { throw e } }
order()
Модификатор order
сортирует строки в указанном порядке.
Сигнатура:
const { data, error } = await supabase .from(table_name) .select(column_names) .order(column_name, options)
column_name
: название колонки, по которой выполняется сортировка;options
:
ascending
: если имеет значениеtrue
, сортировка выполняется по возрастанию;nullsFirst
: еслиtrue
, колонки со значениемnull
идут впереди;foreignTable
: название внешней таблицы.
Пример:
async function getMostLikedPosts(limit) { try { const { data, error } = await supabase .from('posts') .select() .order('like_count', { ascending: true }) .limit(limit) if (error) throw error return data } catch (e) { throw e } }
range()
Модификатор range
возвращает диапазон строк (включительно).
Сигнатура:
const { data, error } = await supabase .from(table_name) .select(column_names) .range(start, end, options)
start
: начало диапазона;end
: конец диапазона;options
:
foreignTable
.
Пример:
async function getPostSlice(from, to) { try { const { data, error } = await supabase .from('posts') .select() .range(from, to) if (error) throw error return data } catch(e) { throw e } }
single()
Модификатор single
возвращает одну строку.
Пример:
async function getUserById(user_id) { try { // в данном случае мы получим объект вместо массива const { data, error } = await supabase .from('users') .select('id, user_name, email') .eq('id', user_id) .single() if (error) throw error return data } catch(e) { throw e } }
Существует также модификатор maybeSingle
, возвращающий хотя бы одну строку.
Фильтры (Filters)
Фильтры могут использоваться в запросах select
, update
и delete
, а также в методе rpc
, когда функция возвращает таблицу.
Фильтры должны применяться в конце запроса. Их можно объединять в цепочки и применять условно.
Примеры:
const { data, error } = await supabase .from('cities') .select() .eq('id', '1342') .single() const { data, error } = await supabase .from('cities') .select() .gte('population', 1000) .lt('population', 10000) const filterByName = null const filterPopLow = 1000 const filterPopHigh = 10000 let query = supabase .from('cities') .select() if (filterByName) { query = query.eq('name', filterByName) } if (filterPopLow) { query = query.gte('population', filterPopLow) } if (filterPopHigh) { query = query.lt('population', filterPopHigh) } const { data, error } = await query
Виды фильтров:
or('filter1, filter2, ...filterN', { foreignTable })
— значение колонки должно удовлетворять хотя бы одному условию, например:or('population.gt.1000, population.lte.100000')
; может использоваться в сочетании с фильтромand
;not(column_name, operator, value)
— значение колонки не должно удовлетворять условию, например:not('population', 'gte', 100000)
;match(query)
— строка должна точно соответствовать объекту, например:match({ user_name: 'Harry', life_style: 'webdev' })
;eq(column_name, column_value)
— строка должна соответствовать условию, например:eq('user_name', 'Harry')
neq(column_name, column_value)
— противоположностьeq
;gt(column_name, value)
— строка должна содержать колонку, которая имеет значение, большее чем указанное (greater than), например:gt('age', 17)
;gte(column_name, value)
,lt(column_name, value)
,lte(column_name, value)
— больше или равно, меньше и меньше или равно, соответственно;like(column_name, pattern)
иilike(column_name, pattern)
— возвращает все строки, значения указанной колонки которых совпадает с паттерном (шаблоном), например,like('user_name', '%oh%')
;is(column_name, value)
— значение колонки должно точно совпадать с указанным значением, например,is('married', false)
;in(column_name, column_value[])
— значение колонки должно совпадать хотя бы с одним из указанных в виде массива значений, например,in('city_name', ['Paris', 'Tokyo'])
;contains(column_name, [column_value])
—contains('main_lang', ['javascript'])
;overlaps(column_name, column_value[])
—overlaps('hobby', ['code', 'guitar'])
;textSearch(column_name, query, options)
— возвращает все строки, значения указанной колонки которой соответствуют запросуto_tsquery
, например:
const { data, error } = await supabase .from('quotes') .select('catchphrase') .textSearch('catchphrase', `'fat' & 'cat'`, { config: 'english' })
Настройки также принимают второй параметр type
, возможными значениями которого являются plain
и phrase
, определяющие базовую и полную нормализацию, соответственно.
filter(column_name, operator, value)
— значение колонки должно удовлетворять фильтру.filter
должен использоваться в последнюю очередь, когда других фильтров оказалось недостаточно:
const { data, error } = await supabase .from('cities') .select('city_name, countries(city_name)') .filter('countries.city_name', 'in', '("France", "Japan")')
Также имеется несколько других (более специфичных) фильтров.
Хранилище (Storage)
Для создания файлового хранилища используется метод createBucket
:
async function createAvatarBucket() { try { const { data, error } = await supabase .storage // createBucket(id, options) .createBucket('avatars', { public: false }) if (error) throw error return data } catch (e) { throw e } }
Для создания хранилища требуются следующие разрешения (permissions policy):
buckets
:insert
;objects
:none
.
В Supabase
безопасность работы с данными и файлами обеспечивается установкой и настройкой row level security (политик защиты строк). Мы рассмотрим их в следующей статье.
Для получения данных о хранилище используется метод getBucket
:
const { data, error } = await supabase .storage // getBucket(id) .getBucket('avatars')
Требуются следующие разрешения:
buckets
:select
;objects
:none
.
Для получения списка хранилищ используется метод listBuckets
:
const { data, error } = await supabase .storage .listBuckets()
Требуются аналогичные разрешения.
Для обновления хранилища используется метод updateBucket(id, options)
(разрешение buckets
: update
), а для удаления — метод deleteBucket(id)
(buckets
: select
и delete
).
Обратите внимание: непустое хранилище не может быть удалено без его предварительной очистки с помощью метода emptyBucket(id)
(buckets
: select
, objects
: select
и delete
).
Загрузка файлов
Для загрузки файлов в хранилище используется метод upload
.
Сигнатура
await supabase .storage .from(bucket_name) .upload(path, file, options)
bucket_name
— названия хранилища;path
— относительный путь к файлу. Должен иметь форматfolder/subfolder/fileName.fileExtension
. Разумеется, записывать файлы можно только в существующие хранилища;file
— загружаемый в хранилище файл: ArrayBuffer | ArrayBufferView | Blob | Buffer | File | FormData | ReadableStream | URLSearchParams | string (т.е. практически что угодно);options
— дополнительные настройки:
cacheControl
: HTTP-заголовокCache-Control
;contentType
: тип содержимого, по умолчанию имеет значениеtext/plain;charset=utf-8
;upsert
: логический индикатор вставки (upsert) файла.
Разрешения:
buckets
:none
;objects
:insert
.
Пример
async function uploadAvatar({ userId, file }) { // получаем расширение файла const fileExt = file.name.split('.')[1] try { const { data, error } = await supabase .storage .from('users') .upload(`avatars/${userId}.${fileExt}`, file, { cacheControl: '3600', upsert: false }) if (error) throw error return data } catch(e) { throw e } }
Скачивание файлов
Для скачивания файлов используется метод download
:
async function downloadFile({ bucketName, filePath }) { try { const { data, error } = await supabase .storage .from(bucketName) .download(filePath) if (error) throw error return data } catch(e) { throw e } }
Разрешения:
buckets
:none
;objects
:select
.
Получение списка файлов
Для получения списка файлов используется метод list
.
Сигнатура
list(dir_name, options, parameters)
dir_name
: название директории;options
:
limit
: количество возвращаемых файлов;offset
: количество пропускаемых файлов;sortBy
:column_name
: название колонки для сортировки;order
: порядок сортировки.
parameters
:
signal
:AbortController.signal
.
Пример
async function getAvatars({ limit = 100, offset = 0, sortBy = { column: 'name', order: 'asc' } }) { try { const { data, error } = await supabase .storage .from('users') .list('avatars', { limit, offset, sortBy }) if (error) throw error return data } catch(e) { throw e } }
Разрешения:
buckets
:none
;objects
:select
.
Обновление файлов
Для обновления файлов используется метод update
.
Сигнатура
update(path, file, options)
options
: настройки, аналогичные передаваемымupload
.
Пример
const defaultUpdateAvatarOptions = { cacheControl: '3600', upsert: false } async function updateAvatar({ filePath, file, options = defaultUpdateAvatarOptions }) { try { const { data, error } = await supabase .storage .from('users') .update(`avatars/${filePath}`, file, options) if (error) throw error return data } catch(e) { throw e } }
Разрешения:
buckets
:none
;objects
:select
иupdate
.
Для перемещения файла с его опциональным переименованием используется метод move
:
const { data, error } = await supabase .storage .from('avatars') .move('public/avatar1.png', 'private/moved_avatar.png')
Разрешения:
buckets
:none
;objects
:select
иupdate
.
Удаление файлов
Для удаления файлов используется метод remove
:
// список удаляемых файлов - массив `filePathArr` async function removeFile({ bucketName, filePathArr }) { try { const { data, error } = await supabase .storage .from(bucketName) .remove(filePathArr) if (error) throw error return data } catch(e) { throw e } }
Разрешения:
buckets
:none
;objects
:select
иdelete
.
Формирование пути к файлу
Для формирования пути к файлу без разрешения используется метод createSignedUrl
:
createSignedUrl(path, expiresIn)
expiresIn
— количество секунд, в течение которых ссылка считается валидной.
async function getSignedUrl({ bucketName, filePath, expiresIn = 60 }) { try { const { signedUrl, error } = await supabase .storage .from(bucketName) .createSignedUrl(filePath, expiresIn) if (error) throw error return signedUrl } catch(e) { throw e } }
Разрешения:
buckets
:none
;objects
:select
.
Для формирования пути к файлу, находящемуся в публичной директории, используется метод getPublicUrl
:
async function getFileUrl({ bucketName, filePath }) { try { const { publicUrl, error } = supabase .storage .from(bucketName) .getPublicUrl(filePath) if (error) throw error return publicUrl } catch(e) { throw e } }
Разрешения не требуются.
Регистрация изменений данных в режиме реального времени (Realtime)
Подписка на изменения
Для подписки на изменения, происходящие в БД, используется метод subscribe
в сочетании с методом on
.
Сигнатура
const subscription = supabase .from(table_name) .on(event, callback)
event
— регистрируемое событие. Для регистрации всех событий используется'*'
;callback
— функция обработки полезной нагрузки.
Обратите внимание:
realtime
отключена по умолчанию для новых проектов по причинам производительности и безопасности;- для того, чтобы получать «предыдущие» данные для обновлений и удалений, необходимо установить
REPLICA IDENTITY
в значениеFULL
, например:ALTER TABLE table_name REPLICA IDENTITY FULL;
.
Примеры
Регистрация всех изменений всех таблиц:
const subscription = supabase .from('*') .on('*', payload => { console.log('***', payload) }) .subscribe()
Регистрация изменений определенной таблицы:
const subscription = supabase .from('posts') .on('*', payload => { console.log('***', payload) }) .subscribe()
Регистрация записи данных в определенную таблицу:
const subscription = supabase .from('posts') // INSERT | UPDATE | DELETE .on('INSERT', payload => { console.log('***', payload) }) .subscribe()
Обработчики можно объединять в цепочки:
const subscription = supabase .from('posts') .on('INSERT', insertHandler) .on('DELETE', deleteHandler) .subscribe()
Имеется возможность регистрации изменений определенных строк. Синтаксис:
table_name:column_name=eq.value
table_name
: название таблицы;column_name
: название колонки;value
: значение, которое должна содержать колонка.
const subscription = supabase .from('countries.id=eq.123') .on('UPDATE', onUpdate) .subscribe()
Отписка от изменений
Метод removeSubscription
удаляет активную подписку и возвращает количество открытых соединений.
supabase.removeSubscription(subscription_name)
subscription_name
— название подписки.
Для удаления всех подписок и закрытия WebSocket-соединения
используется метод removeAllSubscriptions
.
Получение списка подписок
Для получения списка подписок используется метод getSubscriptions
.
const allSubscriptions = supabase.getSubscriptions()
Пожалуй, это все, чем я хотел поделиться с вами в данной статье.
Благодарю за внимание и happy coding!
ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/648761/
Добавить комментарий