Cleaned up Deno implementation
This commit is contained in:
97
FastNotes/package-lock.json
generated
97
FastNotes/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@react-navigation/native": "^7.1.28",
|
||||
"@supabase/supabase-js": "^2.98.0",
|
||||
"async-storage": "^0.1.0",
|
||||
"deno": "^2.7.6",
|
||||
"expo": "~54.0.33",
|
||||
"expo-blur": "~15.0.8",
|
||||
"expo-constants": "~18.0.13",
|
||||
@@ -1556,6 +1557,84 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@deno/darwin-arm64": {
|
||||
"version": "2.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@deno/darwin-arm64/-/darwin-arm64-2.7.6.tgz",
|
||||
"integrity": "sha512-PRRzLK3AfCYthVUEy8/QcCwbWOLt8yt9KLQcnPqi+BoxnRsWHdbDwoJ9xwSigHtD2wOvay98/wU0C+NOiZ8W0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@deno/darwin-x64": {
|
||||
"version": "2.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@deno/darwin-x64/-/darwin-x64-2.7.6.tgz",
|
||||
"integrity": "sha512-xy+jkR5HdGBjdjvRr1EQT62YSrdnYYn6i4k2Jw0hqNIwp2+IyiEbmD3wQ/uQZTt+mMkl+cq2adYuDg4lmGA9vw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@deno/linux-arm64-glibc": {
|
||||
"version": "2.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@deno/linux-arm64-glibc/-/linux-arm64-glibc-2.7.6.tgz",
|
||||
"integrity": "sha512-bMe/5MVeQqzEFleC7oCCxyftUuK/5IM8F66txs33dL+a101QUn3cCSZnor5Ckcf0tY42crFMJ7DiCWuJCP/rtQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@deno/linux-x64-glibc": {
|
||||
"version": "2.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@deno/linux-x64-glibc/-/linux-x64-glibc-2.7.6.tgz",
|
||||
"integrity": "sha512-pyKN9G7qFJWLE2pE3G5Hb05b1ZcjakK3af5U7bk7QQRlKTZGygUTzVlOyzkFh827NpJa3roPaYXsMVZgcml8dw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@deno/win32-arm64": {
|
||||
"version": "2.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@deno/win32-arm64/-/win32-arm64-2.7.6.tgz",
|
||||
"integrity": "sha512-dG29tSiV4gKC9V1+55RovzMfdgKKeVrQYOPUL1xREMWG2+ZacQ++LjwP/RCGyhZXOwvVBLDJw3AK9GXTLBp6Jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@deno/win32-x64": {
|
||||
"version": "2.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@deno/win32-x64/-/win32-x64-2.7.6.tgz",
|
||||
"integrity": "sha512-m6N4rrJUjLyVuZ9YjsdiSHNldbljq9z5+LlTajIHs+JzDaWbs+mXIfhLB4y9cGB73R1msCrE93Q6acnoFlfEFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@egjs/hammerjs": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||
@@ -6308,6 +6387,24 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deno": {
|
||||
"version": "2.7.6",
|
||||
"resolved": "https://registry.npmjs.org/deno/-/deno-2.7.6.tgz",
|
||||
"integrity": "sha512-c1f/1YbZjlIoH+xqRQfz4ir/HcB0h4VBG8OJrM8EANIQxnW+izAylNKzK7jShgNeZtFXkZujZETxa+Rh954xkA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"deno": "bin.cjs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@deno/darwin-arm64": "2.7.6",
|
||||
"@deno/darwin-x64": "2.7.6",
|
||||
"@deno/linux-arm64-glibc": "2.7.6",
|
||||
"@deno/linux-x64-glibc": "2.7.6",
|
||||
"@deno/win32-arm64": "2.7.6",
|
||||
"@deno/win32-x64": "2.7.6"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@react-navigation/native": "^7.1.28",
|
||||
"@supabase/supabase-js": "^2.98.0",
|
||||
"async-storage": "^0.1.0",
|
||||
"deno": "^2.7.6",
|
||||
"expo": "~54.0.33",
|
||||
"expo-blur": "~15.0.8",
|
||||
"expo-constants": "~18.0.13",
|
||||
|
||||
79
FastNotes/supabase/functions/deno.lock
generated
Normal file
79
FastNotes/supabase/functions/deno.lock
generated
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"npm:@supabase/supabase-js@2": "2.99.2"
|
||||
},
|
||||
"npm": {
|
||||
"@supabase/auth-js@2.99.2": {
|
||||
"integrity": "sha512-uRGNXMKEw4VhwouNW7N0XDAGqJP9redHNDmWi17dTrcO1lvFfyRiXsqqfgnVC8aqtRn8kLkLPEzHjiRWsni+oQ==",
|
||||
"dependencies": [
|
||||
"tslib"
|
||||
]
|
||||
},
|
||||
"@supabase/functions-js@2.99.2": {
|
||||
"integrity": "sha512-xuXQARvjdfB1UPK1yUceZ5EGjOLkVz4rBAaloS9foXiAuseWEdgWBCxkIAFRxGBLGX8Uzo8kseq90jhPb+07Vg==",
|
||||
"dependencies": [
|
||||
"tslib"
|
||||
]
|
||||
},
|
||||
"@supabase/postgrest-js@2.99.2": {
|
||||
"integrity": "sha512-ueiOVkbkTQ7RskwVmjR8zxWYw3VKOMxo1+qep+Dx/SgApqyhWBGd92waQb45tbLc7ydB5x8El8utXOLQTuTojQ==",
|
||||
"dependencies": [
|
||||
"tslib"
|
||||
]
|
||||
},
|
||||
"@supabase/realtime-js@2.99.2": {
|
||||
"integrity": "sha512-J6Jm9601dkpZf3+EJ48ki2pM4sFtCNm/BI0l8iEnrczgg+JSEQkMoOW5VSpM54t0pNs69bsz5PTmYJahDZKiIQ==",
|
||||
"dependencies": [
|
||||
"@types/phoenix",
|
||||
"@types/ws",
|
||||
"tslib",
|
||||
"ws"
|
||||
]
|
||||
},
|
||||
"@supabase/storage-js@2.99.2": {
|
||||
"integrity": "sha512-V/FF8kX8JGSefsVCG1spCLSrHdNR/JFeUMn1jS9KG/Eizjx+evtdKQKLJXFgIylY/bKTXKhc2SYDPIGrIhzsug==",
|
||||
"dependencies": [
|
||||
"iceberg-js",
|
||||
"tslib"
|
||||
]
|
||||
},
|
||||
"@supabase/supabase-js@2.99.2": {
|
||||
"integrity": "sha512-179rn5wq0wBAqqGwAwR7TUGg2NOaP+fkd5FCVbYJXby85fsRNPFoNJN8YRBepqX2tN7JJcnTjqaAMXuNjiyisA==",
|
||||
"dependencies": [
|
||||
"@supabase/auth-js",
|
||||
"@supabase/functions-js",
|
||||
"@supabase/postgrest-js",
|
||||
"@supabase/realtime-js",
|
||||
"@supabase/storage-js"
|
||||
]
|
||||
},
|
||||
"@types/node@25.5.0": {
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"dependencies": [
|
||||
"undici-types"
|
||||
]
|
||||
},
|
||||
"@types/phoenix@1.6.7": {
|
||||
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="
|
||||
},
|
||||
"@types/ws@8.18.1": {
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dependencies": [
|
||||
"@types/node"
|
||||
]
|
||||
},
|
||||
"iceberg-js@0.8.1": {
|
||||
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="
|
||||
},
|
||||
"tslib@2.8.1": {
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"undici-types@7.18.2": {
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="
|
||||
},
|
||||
"ws@8.19.0": {
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
// deno-lint-ignore no-import-prefix
|
||||
import { createClient } from "npm:@supabase/supabase-js@2"
|
||||
|
||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? ""
|
||||
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""
|
||||
const EXPO_ACCESS_TOKEN = Deno.env.get("EXPO_ACCESS_TOKEN") ?? ""
|
||||
|
||||
type NoteRecord = {
|
||||
id: number | string
|
||||
created_by: string
|
||||
@@ -38,7 +35,62 @@ type ExpoPushMessage = {
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
|
||||
type ExpoPushTicket = {
|
||||
status?: "ok" | "error"
|
||||
id?: string
|
||||
message?: string
|
||||
details?: {
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
type ExpoPushResponse = {
|
||||
data?: ExpoPushTicket[]
|
||||
}
|
||||
|
||||
type ExpoSendResult = {
|
||||
acceptedCount: number
|
||||
failedTickets: Array<{
|
||||
token: string
|
||||
error: string
|
||||
}>
|
||||
invalidTokens: string[]
|
||||
}
|
||||
|
||||
type SupabaseAdminClient = ReturnType<typeof createClient>
|
||||
|
||||
let supabase: SupabaseAdminClient | null = null
|
||||
|
||||
function getOptionalEnv(name: string) {
|
||||
return Deno.env.get(name)?.trim() ?? ""
|
||||
}
|
||||
|
||||
function requireEnv(name: string) {
|
||||
const value = getOptionalEnv(name)
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`Missing required env var: ${name}`)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function getSupabaseClient() {
|
||||
if (!supabase) {
|
||||
supabase = createClient(
|
||||
requireEnv("SUPABASE_URL"),
|
||||
requireEnv("SUPABASE_SERVICE_ROLE_KEY"),
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return supabase
|
||||
}
|
||||
|
||||
function jsonResponse(status: number, body: Record<string, unknown>) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
@@ -57,16 +109,20 @@ function chunkMessages<T>(items: T[], size: number) {
|
||||
return chunks
|
||||
}
|
||||
|
||||
async function loadCreatorEmail(userId: string): Promise<string> {
|
||||
const { data: profile } = await supabase
|
||||
async function loadCreatorEmail(supabase: SupabaseAdminClient, userId: string): Promise<string> {
|
||||
const { data: profile, error: profileError } = await supabase
|
||||
.from("profiles")
|
||||
.select("email")
|
||||
.eq("id", userId)
|
||||
.maybeSingle()
|
||||
|
||||
if (profileError) {
|
||||
console.error("Failed to load note creator profile:", profileError.message)
|
||||
}
|
||||
|
||||
const typedProfile = profile as ProfileEmailRow | null
|
||||
|
||||
if (typedProfile?.email) {
|
||||
if (typedProfile?.email?.trim()) {
|
||||
return typedProfile.email
|
||||
}
|
||||
|
||||
@@ -80,7 +136,7 @@ async function loadCreatorEmail(userId: string): Promise<string> {
|
||||
return data.user.email ?? "unknown user"
|
||||
}
|
||||
|
||||
async function loadRecipientTokens(userId: string): Promise<string[]> {
|
||||
async function loadRecipientTokens(supabase: SupabaseAdminClient, userId: string): Promise<string[]> {
|
||||
const { data, error } = await supabase
|
||||
.from("user_push_tokens")
|
||||
.select("push_token")
|
||||
@@ -96,14 +152,50 @@ async function loadRecipientTokens(userId: string): Promise<string[]> {
|
||||
return Array.from(new Set(rows.map((row) => row.push_token).filter((token): token is string => Boolean(token))))
|
||||
}
|
||||
|
||||
async function sendExpoPushNotifications(messages: ExpoPushMessage[]) {
|
||||
async function deactivatePushTokens(supabase: SupabaseAdminClient, tokens: string[]) {
|
||||
if (tokens.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const pushTokensTable = supabase.from("user_push_tokens") as any
|
||||
|
||||
const { error } = await pushTokensTable
|
||||
.update({
|
||||
is_active: false,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.in("push_token", tokens)
|
||||
|
||||
if (error) {
|
||||
console.error("Failed to deactivate invalid push tokens:", error.message)
|
||||
}
|
||||
}
|
||||
|
||||
function parseExpoPushResponse(responseText: string): ExpoPushResponse {
|
||||
try {
|
||||
return JSON.parse(responseText) as ExpoPushResponse
|
||||
} catch {
|
||||
throw new Error("Expo push response was not valid JSON.")
|
||||
}
|
||||
}
|
||||
|
||||
async function sendExpoPushNotifications(
|
||||
supabase: SupabaseAdminClient,
|
||||
messages: ExpoPushMessage[],
|
||||
): Promise<ExpoSendResult> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
}
|
||||
|
||||
if (EXPO_ACCESS_TOKEN) {
|
||||
headers.Authorization = `Bearer ${EXPO_ACCESS_TOKEN}`
|
||||
const invalidTokens = new Set<string>()
|
||||
const failedTickets: ExpoSendResult["failedTickets"] = []
|
||||
let acceptedCount = 0
|
||||
const expoAccessToken = getOptionalEnv("EXPO_ACCESS_TOKEN")
|
||||
|
||||
if (expoAccessToken) {
|
||||
headers.Authorization = `Bearer ${expoAccessToken}`
|
||||
}
|
||||
|
||||
for (const chunk of chunkMessages(messages, 100)) {
|
||||
@@ -113,10 +205,48 @@ async function sendExpoPushNotifications(messages: ExpoPushMessage[]) {
|
||||
body: JSON.stringify(chunk),
|
||||
})
|
||||
|
||||
const responseText = await response.text()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
throw new Error(`Expo push request failed with ${response.status}: ${errorBody}`)
|
||||
throw new Error(`Expo push request failed with ${response.status}: ${responseText}`)
|
||||
}
|
||||
|
||||
const responseBody = parseExpoPushResponse(responseText)
|
||||
const tickets = Array.isArray(responseBody.data) ? responseBody.data : []
|
||||
|
||||
if (tickets.length !== chunk.length) {
|
||||
throw new Error(`Expo push response size mismatch: expected ${chunk.length} tickets, got ${tickets.length}.`)
|
||||
}
|
||||
|
||||
for (let index = 0; index < tickets.length; index += 1) {
|
||||
const ticket = tickets[index]
|
||||
const token = chunk[index].to
|
||||
|
||||
if (ticket?.status === "ok") {
|
||||
acceptedCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const expoError = ticket?.details?.error
|
||||
const errorMessage = ticket?.message ?? "Unknown Expo push ticket error."
|
||||
failedTickets.push({
|
||||
token,
|
||||
error: expoError ? `${expoError}: ${errorMessage}` : errorMessage,
|
||||
})
|
||||
|
||||
if (expoError === "DeviceNotRegistered") {
|
||||
invalidTokens.add(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tokensToDeactivate = Array.from(invalidTokens)
|
||||
await deactivatePushTokens(supabase, tokensToDeactivate)
|
||||
|
||||
return {
|
||||
acceptedCount,
|
||||
failedTickets,
|
||||
invalidTokens: tokensToDeactivate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,15 +255,19 @@ Deno.serve(async (request: Request) => {
|
||||
return jsonResponse(405, { error: "Method not allowed" })
|
||||
}
|
||||
|
||||
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
|
||||
return jsonResponse(500, { error: "Missing Supabase environment variables." })
|
||||
}
|
||||
|
||||
let payload: DatabaseWebhookPayload
|
||||
let supabase: SupabaseAdminClient
|
||||
|
||||
try {
|
||||
supabase = getSupabaseClient()
|
||||
payload = await request.json()
|
||||
} catch {
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unexpected error."
|
||||
|
||||
if (message.startsWith("Missing required env var:")) {
|
||||
return jsonResponse(500, { error: message })
|
||||
}
|
||||
|
||||
return jsonResponse(400, { error: "Invalid JSON payload." })
|
||||
}
|
||||
|
||||
@@ -144,8 +278,8 @@ Deno.serve(async (request: Request) => {
|
||||
try {
|
||||
const note = payload.record
|
||||
const [creatorEmail, recipientTokens] = await Promise.all([
|
||||
loadCreatorEmail(note.created_by),
|
||||
loadRecipientTokens(note.created_by),
|
||||
loadCreatorEmail(supabase, note.created_by),
|
||||
loadRecipientTokens(supabase, note.created_by),
|
||||
])
|
||||
|
||||
if (recipientTokens.length === 0) {
|
||||
@@ -165,9 +299,17 @@ Deno.serve(async (request: Request) => {
|
||||
},
|
||||
}))
|
||||
|
||||
await sendExpoPushNotifications(messages)
|
||||
const sendResult = await sendExpoPushNotifications(supabase, messages)
|
||||
|
||||
return jsonResponse(200, { sent: messages.length })
|
||||
if (sendResult.failedTickets.length > 0) {
|
||||
console.error("Expo rejected one or more push messages:", sendResult.failedTickets)
|
||||
}
|
||||
|
||||
return jsonResponse(200, {
|
||||
sent: sendResult.acceptedCount,
|
||||
failed: sendResult.failedTickets.length,
|
||||
deactivated: sendResult.invalidTokens.length,
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unexpected error."
|
||||
console.error("Push notification webhook failed:", message)
|
||||
|
||||
Reference in New Issue
Block a user