diff --git a/.claude/skills/telemetry-standards/SKILL.md b/.claude/skills/telemetry-standards/SKILL.md index 0ed2f7f85bc..b8075526ce1 100644 --- a/.claude/skills/telemetry-standards/SKILL.md +++ b/.claude/skills/telemetry-standards/SKILL.md @@ -14,9 +14,9 @@ reviewing PRs that touch tracking or when implementing new tracking. **Format:** `[object]_[verb]` in snake_case -**Approved verbs only:** -opened, clicked, submitted, created, removed, updated, retrieved, intended, evaluated, added, -enabled, disabled, copied, exposed, failed, converted +**Approved verbs only** (canonical list — derived from `packages/common/telemetry-constants.ts`): +opened, clicked, submitted, created, removed, updated, intended, evaluated, added, +enabled, disabled, copied, exposed, failed, converted, closed, completed, applied, sent, moved **Flag these:** - Unapproved verbs (saved, viewed, seen, pressed, etc.) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000..fbf5c442531 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,26 @@ +# Copilot Code Review Instructions + +## Repo Context + +This is a TypeScript/Next.js/React monorepo: + +- `apps/studio/` — Supabase Dashboard (primary review target) +- `apps/www/` — Marketing site +- `apps/docs/` — Documentation +- `packages/common/` — Shared code including telemetry definitions + +## Topic-Specific Guidelines + +Detailed review rules are in path-specific instruction files under `.github/instructions/`: + +- **Telemetry**: `studio-telemetry.instructions.md` — event naming, property conventions, feature flag measurement +- **Testing**: `studio-testing.instructions.md` — test strategy, extraction patterns, coverage expectations + +These files are scoped to `apps/studio/` and applied automatically by Copilot during reviews. + +## References + +For the full, authoritative versions of these standards: + +- Telemetry: `.claude/skills/telemetry-standards/SKILL.md` +- Testing: `.claude/skills/studio-testing/SKILL.md` diff --git a/.github/instructions/studio-telemetry.instructions.md b/.github/instructions/studio-telemetry.instructions.md new file mode 100644 index 00000000000..3892a6fde6b --- /dev/null +++ b/.github/instructions/studio-telemetry.instructions.md @@ -0,0 +1,59 @@ +--- +applyTo: "apps/studio/**,packages/common/telemetry*" +--- + +# Studio Telemetry Review Rules + +All comments are **advisory** — suggest, do not request changes. + +## When to Flag Missing Telemetry + +Use judgment — not every PR needs telemetry. But **always flag** when: + +1. **Changes to `packages/common/telemetry-constants.ts`** — validate event naming, property conventions, and JSDoc accuracy. +2. **PostHog feature flags without measurement.** If a PR uses `usePHFlag` or PostHog-backed hooks like `useDataApiGrantTogglesEnabled` to gate behavior, the flag state should be captured in a telemetry event so the rollout can be measured. Flag if the flag value isn't included in a relevant `track()` call. (Note: `useFlag` from `common` reads ConfigCat flags, not PostHog — different system, different guidance.) +3. **Feature-flagged rollouts without outcome tracking.** If a flag gates new behavior, there should be telemetry on both the flag state *and* how users respond to the new behavior (e.g., toggle clicks, opt-in actions). +4. **Growth-oriented components adding user interactions without tracking** — onboarding flows, setup wizards, upgrade CTAs, A/B experiment variants. + +When tracking is missing, comment: _"This adds a user interaction (or feature flag) that may benefit from tracking."_ Then propose an event name and `useTrack()` call. + +## Feature Flag Telemetry Pattern + +When capturing a PostHog flag value for telemetry, read the raw flag via `usePHFlag('flagName')` — **not** through wrapper hooks that coerce `undefined` to `false`. Use conditional spread so the property is omitted (not false) when the flag store hasn't loaded: + +```typescript +const flagValue = usePHFlag('myBooleanFlag') // for boolean flags +track('event_name', { + ...(flagValue !== undefined && { myFlagEnabled: flagValue }), +}) +``` + +For string-valued flags (e.g., experiment variants), use `usePHFlag('flagName')` instead. + +## Event Naming + +Format: `[object]_[verb]` in snake_case. + +Prefer verbs already in use in `packages/common/telemetry-constants.ts`: `opened`, `clicked`, `submitted`, `created`, `removed`, `updated`, `intended`, `evaluated`, `added`, `enabled`, `disabled`, `copied`, `exposed`, `failed`, `converted`, `closed`, `completed`, `applied`, `sent`, `moved`. + +Flag: unapproved verbs (`saved`, `viewed`, `pressed`), wrong order (`click_product_card`), wrong casing (`productCardClicked`), passive view tracking on page load (exception: `_exposed` events for A/B experiments). + +## Event Properties + +- **camelCase** for new events; match existing convention when extending +- Self-explanatory names — flag generic (`label`, `value`, `name`, `data`) +- Check `telemetry-constants.ts` for consistency with similar events +- Never track PII + +## Event Implementation + +- Use `useTrack` from `lib/telemetry/track` — avoid introducing new `useSendEventMutation` usage +- New events need a TypeScript interface in `telemetry-constants.ts` with `@group Events` and `@source` JSDoc tags (add `@page` when applicable for page-specific events), added to the `TelemetryEvent` union + +```typescript +import { useTrack } from 'lib/telemetry/track' +const track = useTrack() +track('product_card_clicked', { productType: 'database', planTier: 'pro' }) +``` + +Canonical standards: `.claude/skills/telemetry-standards/SKILL.md` diff --git a/.github/instructions/studio-testing.instructions.md b/.github/instructions/studio-testing.instructions.md new file mode 100644 index 00000000000..0bd11a4b948 --- /dev/null +++ b/.github/instructions/studio-testing.instructions.md @@ -0,0 +1,29 @@ +--- +applyTo: "apps/studio/**" +--- + +# Studio Testing Review Rules + +All comments are **advisory**. + +## Core Principle + +Push logic out of React components into pure `.utils.ts` functions, then test those functions exhaustively. Only use component tests for complex UI interactions. + +## When to Comment + +- PR adds **business logic inline in a component** that could be extracted to a `ComponentName.utils.ts` file next to the component and unit tested at `tests/components/.../ComponentName.utils.test.ts` +- PR adds a **utility function without test coverage** +- PR uses **component tests for pure logic** that should be a unit test on a pure function +- PR adds a **feature used in both self-hosted and platform** without E2E test consideration + +## Which Test Type to Suggest + +- **Pure transformation** (parse, format, validate, compute) → extract to `.utils.ts` + unit test with vitest +- **Complex UI interaction** → component test with `customRender` (or E2E if shared with self-hosted) +- **E2E tests** should cover both click interactions AND keyboard shortcuts +- **No tests at all** for non-trivial changes → nudge to add coverage + +## Reference + +See `.claude/skills/studio-testing/SKILL.md` for the full testing standard. diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index b42c8cc6b88..8596073979f 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -22,8 +22,10 @@ jobs: with: sparse-checkout: | apps - packages blocks + examples + i18n + packages patches - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 name: Install pnpm @@ -40,56 +42,3 @@ jobs: - name: Run prettier run: |- pnpm run test:prettier - - # i18n is not a node package, so we handle that one separately - format-i18n: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Check out repo - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - sparse-checkout: | - i18n - patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Install pnpm - with: - run_install: false - - name: Setup node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - cache: 'pnpm' - - name: Download dependencies - run: | - pnpm install --frozen-lockfile - - name: Run prettier - run: |- - pnpm exec prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}' - - format-sql: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Check out repo - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - sparse-checkout: | - apps/docs/pages - apps/docs/content - patches - - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Install pnpm - with: - run_install: false - - name: Setup node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - cache: 'pnpm' - - name: Download dependencies - run: | - pnpm install --frozen-lockfile - - name: Run prettier - run: |- - # Check mdx files which contain sql code blocks - grep -lr '```sql' apps/docs/{pages,content}/**/*.mdx | xargs pnpm exec prettier -c diff --git a/.github/workflows/self-host-tests-smoke.yml b/.github/workflows/self-host-tests-smoke.yml index 19228e752b5..49abfeb51d2 100644 --- a/.github/workflows/self-host-tests-smoke.yml +++ b/.github/workflows/self-host-tests-smoke.yml @@ -25,4 +25,4 @@ jobs: docker/ - name: Run docker-compose up # Ensure all services can be started and healthy with default config - run: cd docker && cp .env.example .env && docker compose up --wait + run: cd docker && cp .env.example .env && docker compose up --wait --wait-timeout 180 diff --git a/apps/design-system/content/docs/ui-patterns/modality.mdx b/apps/design-system/content/docs/ui-patterns/modality.mdx index eca2d40bdce..9c4358cce0c 100644 --- a/apps/design-system/content/docs/ui-patterns/modality.mdx +++ b/apps/design-system/content/docs/ui-patterns/modality.mdx @@ -96,6 +96,7 @@ Implementation checklist: - Render a separate discard confirmation dialog when dirty. - Keep `Cancel` non-destructive; use `Discard`/`Discard changes` for one-click destructive exits. - Guard controlled close attempts only; do not try to block route changes or arbitrary unmounts. +- If dismissal is route-driven or tied to page unload, use a navigation guard that composes the same discard-confirmation UI instead of extending the dialog/sheet close guard. Studio implementation (preferred in Studio code): diff --git a/apps/studio/pages/api/incident-status.ts b/apps/studio/app/api/incident-status/route.ts similarity index 72% rename from apps/studio/pages/api/incident-status.ts rename to apps/studio/app/api/incident-status/route.ts index ded8eb02d98..456806054a9 100644 --- a/apps/studio/pages/api/incident-status.ts +++ b/apps/studio/app/api/incident-status/route.ts @@ -1,5 +1,5 @@ import { IS_PLATFORM } from 'common' -import { NextApiRequest, NextApiResponse } from 'next' +import { NextResponse } from 'next/server' import { InternalServerError } from '@/lib/api/apiHelpers' import { getActiveIncidents, type IncidentCache } from '@/lib/api/incident-status' @@ -42,22 +42,26 @@ async function fetchIncidentCache(incidentIds: Array): Promise { - const sort = searchParams.getAll('sort') - const filter = searchParams.getAll('filter') + const sort = searchParams?.getAll('sort') ?? [] + const filter = searchParams?.getAll('filter') ?? [] return { sort, filter } }, [searchParams]) diff --git a/apps/studio/components/grid/components/grid/ColumnHeader.tsx b/apps/studio/components/grid/components/grid/ColumnHeader.tsx index aa4492882b5..68145498442 100644 --- a/apps/studio/components/grid/components/grid/ColumnHeader.tsx +++ b/apps/studio/components/grid/components/grid/ColumnHeader.tsx @@ -1,12 +1,12 @@ +import { FOREIGN_KEY_CASCADE_ACTION } from '@supabase/pg-meta' +import { getForeignKeyCascadeAction } from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils' import type { XYCoord } from 'dnd-core' import { ArrowRight, Key, Lightbulb, Link, Lock } from 'lucide-react' import { useEffect, useRef } from 'react' import { useDrag, useDrop } from 'react-dnd' - -import { getForeignKeyCascadeAction } from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' + import { useColumnHasIndexSuggestion, useTableIndexAdvisor, diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx index d72c035224c..2b2170cac67 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx @@ -1,7 +1,5 @@ +import { zodResolver } from '@hookform/resolvers/zod' import { useQueryClient } from '@tanstack/react-query' -import { Dispatch, SetStateAction, useEffect, useState } from 'react' -import { toast } from 'sonner' - import { LOCAL_STORAGE_KEYS } from 'common' import InformationBox from 'components/ui/InformationBox' import { organizationKeys } from 'data/organizations/keys' @@ -9,9 +7,16 @@ import { useMfaChallengeAndVerifyMutation } from 'data/profile/mfa-challenge-and import { useMfaEnrollMutation } from 'data/profile/mfa-enroll-mutation' import { useMfaUnenrollMutation } from 'data/profile/mfa-unenroll-mutation' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -import { Input } from 'ui' +import { useEffect, useState } from 'react' +import { useForm, type SubmitHandler } from 'react-hook-form' +import { toast } from 'sonner' +import { Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input, Input_Shadcn_ } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import { z } from 'zod' + +type TOTP = { qr_code: string; secret: string; uri: string } interface AddNewFactorModalProps { visible: boolean @@ -19,32 +24,24 @@ interface AddNewFactorModalProps { } export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => { - // Generate a name with a number between 0 and 1000 - const [name, setName] = useState(`App ${Math.floor(Math.random() * 1000)}`) const { data, mutate: enroll, isPending: isEnrolling, reset } = useMfaEnrollMutation() useEffect(() => { - // reset has to be called because the state is kept between if the process is canceled during - // the second step. - if (!visible) { - setName(`App ${Math.floor(Math.random() * 1000)}`) - reset() - } + if (!visible) reset() }, [reset, visible]) return ( <> } isLoading={isEnrolling} onClose={onClose} @@ -55,14 +52,33 @@ export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) interface FirstStepProps { visible: boolean - name: string - setName: Dispatch> - enroll: (params: { factorType: 'totp'; friendlyName?: string }) => void isEnrolling: boolean + reset: () => void + enroll: (params: { factorType: 'totp'; friendlyName?: string }) => void onClose: () => void } -const FirstStep = ({ visible, name, enroll, setName, isEnrolling, onClose }: FirstStepProps) => { +const FirstStep = ({ visible, isEnrolling, reset, enroll, onClose }: FirstStepProps) => { + const FormSchema = z.object({ + name: z.string().min(1, 'Please provide a name to identify this app'), + }) + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { name: '' }, + mode: 'onChange', + }) + + const onSubmit: SubmitHandler> = async (values) => { + enroll({ factorType: 'totp', friendlyName: values.name }) + } + + useEffect(() => { + if (!visible) { + // Generate a name with a number between 0 and 1000 + form.reset({ name: `App ${Math.floor(Math.random() * 1000)}` }) + } + }, [form, visible]) + return ( { - enroll({ - factorType: 'totp', - friendlyName: name, - }) - }} + onConfirm={form.handleSubmit(onSubmit)} > - setName(e.target.value)} - /> + +
+ ( + + + + + + )} + /> + +
) } @@ -96,11 +124,7 @@ interface SecondStepProps { factor?: { id: string type: 'totp' - totp: { - qr_code: string - secret: string - uri: string - } + totp: TOTP } isLoading: boolean onClose: () => void @@ -113,14 +137,23 @@ const SecondStep = ({ isLoading, onClose, }: SecondStepProps) => { - const [code, setCode] = useState('') const queryClient = useQueryClient() - const [lastVisitedOrganization] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, '' ) + const FormSchema = z.object({ + code: z.string().min(1, 'Please provide a code from your authenticator app'), + }) + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { code: '' }, + mode: 'onChange', + }) + + const [factor, setFactor] = useState<{ id: string; type: 'totp'; totp: TOTP } | null>(null) + const { mutate: unenroll } = useMfaUnenrollMutation({ onSuccess: () => onClose() }) const { mutate: challengeAndVerify, isPending: isVerifying } = useMfaChallengeAndVerifyMutation({ onError: (error) => { @@ -137,15 +170,10 @@ const SecondStep = ({ }, }) - const [factor, setFactor] = useState<{ - id: string - type: 'totp' - totp: { - qr_code: string - secret: string - uri: string - } - } | null>(null) + const onSubmit: SubmitHandler> = async (values) => { + if (!factor) return toast.error('Factor required') + challengeAndVerify({ factorId: factor.id, code: values.code }) + } // this useEffect is to keep the factor until a new one comes. This is a fix to an issue which // happens when closing the modal, the outer factor is reset to null too soon and the modal @@ -153,6 +181,7 @@ const SecondStep = ({ useEffect(() => { if (outerFactor && factor?.id !== outerFactor.id) { setFactor(outerFactor) + form.reset({ code: '' }) } }, [outerFactor]) @@ -160,6 +189,7 @@ const SecondStep = ({ factor && challengeAndVerify({ factorId: factor.id, code })} + onConfirm={form.handleSubmit(onSubmit)} > -
- - Use an authenticator app to scan the following QR code, and provide the code from the app - to complete the enrolment. - -
+

+ Use an authenticator app to scan the following QR code, and provide the code from the app to + complete the enrolment. +

+ {isLoading && (
)} + {factor && ( - <> +
{factor.totp.uri}
-
- - } - /> -
-
- setCode(e.target.value)} - /> -
- + + + } + /> + + +
+ ( + + + + + + )} + /> + +
+
)}
) diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index a7672b64b28..60cbac634f9 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -65,10 +65,11 @@ export const FeaturePreviewModal = () => { const featurePreviewContext = useFeaturePreviewContext() const { mutate: sendEvent } = useSendEventMutation() - const [isDismissedTableFilterBar, setIsDismissedTableFilterBar] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''), - false - ) + const [isDismissedTableQueueOperations, setIsDismissedTableQueueOperations] = + useLocalStorageQuery( + LOCAL_STORAGE_KEYS.TABLE_EDITOR_QUEUE_OPERATIONS_BANNER_DISMISSED(ref ?? ''), + false + ) const { flags, onUpdateFlag } = featurePreviewContext const allFeaturePreviews = ( @@ -90,10 +91,10 @@ export const FeaturePreviewModal = () => { }) if ( - selectedFeature.key === LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR && - !isDismissedTableFilterBar + selectedFeature.key === LOCAL_STORAGE_KEYS.UI_PREVIEW_QUEUE_OPERATIONS && + !isDismissedTableQueueOperations ) { - setIsDismissedTableFilterBar(true) + setIsDismissedTableQueueOperations(true) } } diff --git a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.constants.test.ts b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.constants.test.ts new file mode 100644 index 00000000000..3a5508f00a0 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.constants.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' + +import { FormSchema } from './EditHookPanel.constants' + +describe('EditHookPanel FormSchema', () => { + it('rejects incomplete http_request hostnames', () => { + const result = FormSchema.safeParse({ + name: 'Test hook', + table_id: 'public.messages', + http_method: 'POST' as const, + timeout_ms: 1000, + events: ['INSERT'], + httpHeaders: [], + httpParameters: [], + function_type: 'http_request' as const, + http_url: 'https://webhook', + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect( + result.error.issues.some((issue) => issue.message === 'Please provide a valid URL') + ).toBe(true) + } + }) + + it('rejects http_request URLs without an explicit protocol', () => { + const result = FormSchema.safeParse({ + name: 'Test hook', + table_id: 'public.messages', + http_method: 'POST' as const, + timeout_ms: 1000, + events: ['INSERT'], + httpHeaders: [], + httpParameters: [], + function_type: 'http_request' as const, + http_url: 'hooks.example.com/webhook', + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect( + result.error.issues.some( + (issue) => issue.message === 'Please prefix your URL with http:// or https://' + ) + ).toBe(true) + } + }) +}) diff --git a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.constants.ts b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.constants.ts index fb991e2c17b..87ece7fc9d0 100644 --- a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.constants.ts +++ b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.constants.ts @@ -1,14 +1,14 @@ import { z } from 'zod' -import { isValidHttpUrl } from '@/lib/helpers' +import { httpEndpointUrlSchema } from '@/lib/validation/http-url' const httpRequestSchema = z.object({ function_type: z.literal('http_request'), - http_url: z - .string() - .min(1, 'Please provide a URL') - .refine((val) => val.startsWith('http'), 'Please prefix your URL with http or https') - .refine((val) => isValidHttpUrl(val), 'Please provide a valid URL'), + http_url: httpEndpointUrlSchema({ + requiredMessage: 'Please provide a URL', + invalidMessage: 'Please provide a valid URL', + prefixMessage: 'Please prefix your URL with http:// or https://', + }), }) const supabaseFunctionSchema = z.object({ diff --git a/apps/studio/components/interfaces/Home/AdvisorWidget.tsx b/apps/studio/components/interfaces/Home/AdvisorWidget.tsx index e81295f82f7..47b6cfc2a91 100644 --- a/apps/studio/components/interfaces/Home/AdvisorWidget.tsx +++ b/apps/studio/components/interfaces/Home/AdvisorWidget.tsx @@ -1,7 +1,6 @@ import { useParams } from 'common' import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants' import { createLintSummaryPrompt, EntityTypeIcon } from 'components/interfaces/Linter/Linter.utils' -import { useQueryPerformanceQuery } from 'components/interfaces/Reports/Reports.queries' import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { AiAssistantDropdown } from 'components/ui/AiAssistantDropdown' import { ButtonTooltip } from 'components/ui/ButtonTooltip' @@ -31,6 +30,8 @@ import { } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' +import { useQueryPerformanceQuery } from '../QueryPerformance/useQueryPerformanceQuery' + interface SlowQuery { rolname: string mean_time: number @@ -43,9 +44,7 @@ export const AdvisorWidget = () => { const [selectedTab, setSelectedTab] = useState<'security' | 'performance'>('security') const { data: lints, isPending: isLoadingLints } = useProjectLintsQuery({ projectRef }) const { data: slowestQueriesData, isLoading: isLoadingSlowestQueries } = useQueryPerformanceQuery( - { - preset: 'slowestExecutionTime', - } + { preset: 'slowestExecutionTime' } ) const snap = useAiAssistantStateSnapshot() const { openSidebar } = useSidebarManagerSnapshot() diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.test.ts new file mode 100644 index 00000000000..f048b52e4ab --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' + +import { FormSchema } from './CreateCronJobSheet.constants' + +describe('CreateCronJobSheet FormSchema', () => { + it('rejects incomplete http_request hostnames', () => { + const result = FormSchema.safeParse({ + name: 'Send webhook', + supportsSeconds: false, + schedule: '* * * * *', + values: { + type: 'http_request' as const, + method: 'POST' as const, + endpoint: 'https://webhook', + timeoutMs: 1000, + httpHeaders: [], + snippet: '', + }, + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect( + result.error.issues.some((issue) => issue.message === 'Please provide a valid URL') + ).toBe(true) + } + }) + + it('rejects http_request URLs without an explicit protocol', () => { + const result = FormSchema.safeParse({ + name: 'Send webhook', + supportsSeconds: false, + schedule: '* * * * *', + values: { + type: 'http_request' as const, + method: 'POST' as const, + endpoint: 'hooks.example.com/webhook', + timeoutMs: 1000, + httpHeaders: [], + snippet: '', + }, + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect( + result.error.issues.some( + (issue) => issue.message === 'Please prefix your URL with http:// or https://' + ) + ).toBe(true) + } + }) +}) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts index a9f70ce8c2a..ddbdf55928b 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.constants.ts @@ -1,8 +1,8 @@ import { toString as CronToString } from 'cronstrue' import z from 'zod' -import { urlRegex } from 'components/interfaces/Auth/Auth.constants' import { cronPattern, secondsPattern } from '../CronJobs.constants' +import { httpEndpointUrlSchema } from '@/lib/validation/http-url' const convertCronToString = (schedule: string) => { // pg_cron can also use "30 seconds" format for schedule. Cronstrue doesn't understand that format so just use the @@ -40,12 +40,11 @@ const edgeFunctionSchema = z.object({ const httpRequestSchema = z.object({ type: z.literal('http_request'), method: z.enum(['GET', 'POST']), - endpoint: z - .string() - .trim() - .min(1, 'Please provide a URL') - .regex(urlRegex(), 'Please provide a valid URL') - .refine((value) => value.startsWith('http'), 'Please include HTTP/HTTPs to your URL'), + endpoint: httpEndpointUrlSchema({ + requiredMessage: 'Please provide a URL', + invalidMessage: 'Please provide a valid URL', + prefixMessage: 'Please prefix your URL with http:// or https://', + }), timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000), httpHeaders: z.array(z.object({ name: z.string(), value: z.string() })), httpBody: z diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx index c830c8afc9e..9def10fbcf7 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.constants.tsx @@ -46,12 +46,21 @@ export type HTTPHeader = { name: string; value: string } export type HTTPParameter = { name: string; value: string } -export const CRON_TABLE_COLUMNS = [ - { id: 'jobname', name: 'Name', minWidth: 0, width: 200 }, - { id: 'schedule', name: 'Schedule', width: 100 }, - { id: 'latest_run', name: 'Last run', width: 265 }, - { id: 'next_run', name: 'Next run', minWidth: 180 }, - { id: 'command', name: 'Command', minWidth: 320 }, - { id: 'active', name: 'Active', width: 70, minWidth: 70, maxWidth: 70 }, - { id: 'actions', name: '', minWidth: 75, width: 75 }, +type CronTableColumn = { + id: string + name: string + width?: number + minWidth?: number + maxWidth?: number + resizable?: boolean +} + +export const CRON_TABLE_COLUMNS: CronTableColumn[] = [ + { id: 'jobname', name: 'Name', minWidth: 160, width: 200, resizable: true }, + { id: 'schedule', name: 'Schedule', width: 100, resizable: true }, + { id: 'latest_run', name: 'Last run', width: 265, resizable: true }, + { id: 'next_run', name: 'Next run', minWidth: 180, resizable: true }, + { id: 'command', name: 'Command', minWidth: 320, resizable: true }, + { id: 'active', name: 'Active', width: 70, minWidth: 70, maxWidth: 70, resizable: false }, + { id: 'actions', name: '', minWidth: 75, width: 75, resizable: false }, ] diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts index 158a9c18c1b..143b76de9de 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { cronPattern, secondsPattern } from './CronJobs.constants' -import { parseCronJobCommand } from './CronJobs.utils' +import { formatCronJobColumns, parseCronJobCommand } from './CronJobs.utils' describe('parseCronJobCommand', () => { it('should return a default object when the command is null', () => { @@ -279,3 +279,23 @@ describe('parseCronJobCommand', () => { }) }) }) + +describe('formatCronJobColumns', () => { + it('enables resizing for informational columns and keeps utility columns fixed', () => { + const columns = formatCronJobColumns({ + onSelectEdit: () => undefined, + onSelectDelete: () => undefined, + }) + + const columnsByKey = Object.fromEntries(columns.map((column) => [String(column.key), column])) + + expect(columnsByKey.jobname.resizable).toBe(true) + expect(columnsByKey.jobname.minWidth).toBeGreaterThan(0) + expect(columnsByKey.schedule.resizable).toBe(true) + expect(columnsByKey.latest_run.resizable).toBe(true) + expect(columnsByKey.next_run.resizable).toBe(true) + expect(columnsByKey.command.resizable).toBe(true) + expect(columnsByKey.active.resizable).toBe(false) + expect(columnsByKey.actions.resizable).toBe(false) + }) +}) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx index 75ef9acf94e..1a1cfb6baed 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx @@ -236,7 +236,7 @@ export const formatCronJobColumns = ({ minWidth: col.minWidth ?? 100, maxWidth: col.maxWidth, width: col.width, - resizable: false, + resizable: col.resizable ?? false, sortable: false, draggable: false, headerCellClass: undefined, diff --git a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx index 25a0e115196..f5a8572ded4 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx @@ -5,13 +5,13 @@ import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { EMPTY_ARR } from 'lib/void' import { useMemo } from 'react' +import { parseSchemaComment } from 'stripe-experiment-sync/supabase' import { wrapperMetaComparator } from '../Wrappers/Wrappers.utils' import { INTEGRATIONS } from './Integrations.constants' import { isInstalled as checkIsInstalled, findStripeSchema, - parseStripeSchema, } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status' export const useInstalledIntegrations = () => { @@ -77,7 +77,7 @@ export const useInstalledIntegrations = () => { } if (integration.id === 'stripe_sync_engine') { const stripeSchema = findStripeSchema(schemas) - const parsedSchema = parseStripeSchema(stripeSchema) + const parsedSchema = parseSchemaComment(stripeSchema?.comment) return checkIsInstalled(parsedSchema.status) } if (integration.type === 'wrapper') { diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationError.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationError.tsx index 2c8e63d7b5c..90bf85d1f8f 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationError.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationError.tsx @@ -1,37 +1,50 @@ import { useParams } from 'common' import { Button } from 'ui' -import { Admonition } from 'ui-patterns' -import { ContactSupportButton } from '@/components/ui/AlertError' +import { useStripeSyncStatus } from './useStripeSyncStatus' +import AlertError from '@/components/ui/AlertError' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' export const InstallationError = ({ error, handleUninstall, handleOpenInstallSheet, + isUpgrade, + installing, + uninstalling, }: { error: 'install' | 'uninstall' handleUninstall: () => void handleOpenInstallSheet: () => void + isUpgrade?: boolean + installing?: boolean + uninstalling?: boolean }) => { - const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() + + const { + schemaComment: { errorMessage }, + } = useStripeSyncStatus({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) if (error === 'uninstall') { return ( - - - - + additionalActions={ + } /> ) @@ -39,18 +52,27 @@ export const InstallationError = ({ if (error === 'install') { return ( - - - - + + {isUpgrade ? 'Retry upgrade' : 'Retry installation'} + } /> ) diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx index d1a3343376f..fbcd7c929b6 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx @@ -45,8 +45,6 @@ import { } from './stripe-sync-status' import { StripeSyncChangesCard } from './StripeSyncChangesCard' import { useStripeSyncStatus } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus' -import { InlineLink } from '@/components/ui/InlineLink' -import { useSSLEnforcementQuery } from '@/data/ssl-enforcement/ssl-enforcement-query' const installFormSchema = z.object({ stripeSecretKey: z.string().min(1, 'Stripe API key is required'), @@ -71,7 +69,10 @@ export const StripeSyncInstallationPage = () => { }) const { - parsedSchema: { status: installationStatus }, + schemaComment, + schemaComment: { status: installationStatus }, + latestAvailableVersion, + timedOut, } = useStripeSyncStatus({ projectRef: project?.ref, connectionString: project?.connectionString, @@ -91,6 +92,22 @@ export const StripeSyncInstallationPage = () => { const installDone = isInstallDone(installationStatus) const uninstallDone = isUninstallDone(installationStatus) + // Detect if this is an upgrade (both old and new versions present) + let oldVersion + let newVersion + if (installed) { + // when installed we compare the installed version against the latest available + oldVersion = schemaComment?.newVersion + newVersion = latestAvailableVersion + } else { + // otherwise compare the old and new versions from the schema + oldVersion = schemaComment?.oldVersion + newVersion = schemaComment?.newVersion + } + + const upgradeAvailable = !!(oldVersion && newVersion && oldVersion !== newVersion) + const upgradeDone = latestAvailableVersion == schemaComment?.newVersion + const { mutate: installStripeSync, isPending: isInstallRequested, @@ -98,7 +115,9 @@ export const StripeSyncInstallationPage = () => { reset: resetInstallError, } = useStripeSyncInstallMutation({ onSuccess: () => { - toast.success('Stripe Sync installation started') + toast.success( + upgradeAvailable ? 'Stripe Sync upgrade started' : 'Stripe Sync installation started' + ) setShouldShowInstallSheet(false) form.reset() setIsInstallInitiated(true) @@ -115,11 +134,12 @@ export const StripeSyncInstallationPage = () => { }) // Combine schema status with mutation/initiated states for UI - const installing = installInProgress || isInstallRequested || isInstallInitiated - const uninstalling = uninstallInProgress || isUninstallRequested || isUninstallInitiated + const installing = (installInProgress || isInstallRequested || isInstallInitiated) && !timedOut + const uninstalling = + (uninstallInProgress || isUninstallRequested || isUninstallInitiated) && !timedOut const canInstall = checkCanInstall(installationStatus) && !installed && !installing - const hasError = (uninstallError || installError) && !uninstalling && !installing + const hasError = (uninstallError || installError) && ((!uninstalling && !installing) || timedOut) // Poll for schema changes during transitions useSchemasQuery( @@ -127,19 +147,12 @@ export const StripeSyncInstallationPage = () => { { refetchInterval: installing || uninstalling ? 5000 : false } ) - const { data: sslEnforcementConfiguration, isSuccess: isSuccessSslEnforcement } = - useSSLEnforcementQuery({ - projectRef: project?.ref, - }) - const isSSLEnforced = - sslEnforcementConfiguration?.appliedSuccessfully && - sslEnforcementConfiguration?.currentConfig.database - const handleUninstall = useCallback(() => { if (!project?.ref) return uninstallStripeSync({ projectRef: project.ref, + startTime: Date.now(), }) }, [project?.ref, uninstallStripeSync]) @@ -173,12 +186,13 @@ export const StripeSyncInstallationPage = () => { } }, [installError, track]) - // Clear install initiated flag once schema reflects completion or error + // Clear install initiated flag once schema reflects successful completion + // For errors, the flag is cleared when user manually retries (handleOpenInstallSheet) useEffect(() => { - if (isInstallInitiated && installDone) { + if (isInstallInitiated && installDone && upgradeDone && !installError) { setIsInstallInitiated(false) } - }, [isInstallInitiated, installDone]) + }, [isInstallInitiated, installDone, upgradeDone, installError]) // Clear uninstall initiated flag once schema is removed or error useEffect(() => { @@ -195,20 +209,30 @@ export const StripeSyncInstallationPage = () => { error={uninstallError ? 'uninstall' : 'install'} handleUninstall={handleUninstall} handleOpenInstallSheet={handleOpenInstallSheet} + isUpgrade={upgradeAvailable} + installing={installing} + uninstalling={uninstalling} /> ) : null } status={ } actions={ !installed && !uninstalling && !uninstallError ? ( <> - +
{ }, }} > - {installError ? 'Retry installation' : 'Install integration'} + {installError + ? upgradeAvailable + ? 'Retry upgrade' + : 'Retry installation' + : upgradeAvailable + ? 'Upgrade integration' + : 'Install integration'} {installError && ( diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx index da6e6ee44c7..488845d21c8 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx @@ -1,4 +1,5 @@ import { AlertCircle, Check, RefreshCwIcon } from 'lucide-react' +import { SchemaInstallationStatus } from 'stripe-experiment-sync/supabase' import { hasInstallError, @@ -6,17 +7,24 @@ import { isInstalled, isInstalling, isUninstalling, - StripeInstallationStatus, } from './stripe-sync-status' export const StatusDisplay = ({ status, isInstallRequested, + isInstallInitiated, isUninstallRequested, + isUninstallInitiated, + isUpgrade, + timedOut, }: { - status: StripeInstallationStatus + status: SchemaInstallationStatus isInstallRequested: boolean + isInstallInitiated: boolean isUninstallRequested: boolean + isUninstallInitiated: boolean + isUpgrade?: boolean + timedOut: boolean }) => { const installed = isInstalled(status) const installError = hasInstallError(status) @@ -24,17 +32,10 @@ export const StatusDisplay = ({ const installInProgress = isInstalling(status) const uninstallInProgress = isUninstalling(status) - const installing = installInProgress || isInstallRequested - const uninstalling = uninstallInProgress || isUninstallRequested + const installing = (installInProgress || isInstallRequested || isInstallInitiated) && !timedOut + const uninstalling = + (uninstallInProgress || isUninstallRequested || isUninstallInitiated) && !timedOut - if (uninstallError) { - return ( - - - Uninstallation error - - ) - } if (uninstalling) { return ( @@ -43,11 +44,11 @@ export const StatusDisplay = ({ ) } - if (installError) { + if (uninstallError) { return ( - Installation error + Uninstallation error ) } @@ -55,7 +56,15 @@ export const StatusDisplay = ({ return ( - Installing... + {isUpgrade ? 'Upgrading...' : 'Installing...'} + + ) + } + if (installError) { + return ( + + + {isUpgrade ? 'Upgrade error' : 'Installation error'} ) } diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx index 98db11118c3..a0bb8094a39 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx @@ -1,5 +1,6 @@ import { EdgeFunctions } from 'icons' -import { Layers, Table } from 'lucide-react' +import { Layers, Loader2, Table } from 'lucide-react' +import { SchemaInstallationStatus } from 'stripe-experiment-sync/supabase' import { Card, CardContent, cn } from 'ui' import { @@ -10,12 +11,12 @@ import { isInstalling, isUninstallDone, isUninstalling, - StripeInstallationStatus, } from './stripe-sync-status' type StripeSyncChangesCardProps = { - installationStatus: StripeInstallationStatus + installationStatus: SchemaInstallationStatus className?: string + isUpgrade?: boolean } const ListItemClassName = 'flex items-center gap-x-3 py-2 px-3 border-b' @@ -23,6 +24,7 @@ const ListItemClassName = 'flex items-center gap-x-3 py-2 px-3 border-b' export const StripeSyncChangesCard = ({ installationStatus, className, + isUpgrade, }: StripeSyncChangesCardProps) => { const installed = isInstalled(installationStatus) const installError = hasInstallError(installationStatus) @@ -32,53 +34,70 @@ export const StripeSyncChangesCard = ({ const installDone = isInstallDone(installationStatus) const uninstallDone = isUninstallDone(installationStatus) - const title = - uninstallDone || installError + // Special case: installed integration with upgrade available (shown in sheet context) + const isInProgress = installInProgress || uninstallInProgress + const isInstalledWithUpgrade = installed && isUpgrade + + const title = isInstalledWithUpgrade + ? 'This integration will upgrade your Supabase project:' + : uninstallDone || installError ? 'This integration will modify your Supabase project:' - : installInProgress || uninstallInProgress + : isInProgress ? 'This integration is modifying your Supabase project:' : installDone || installed || uninstallError ? 'This integration has modified your Supabase project:' : '' - const dbLine = - uninstallDone || installError - ? 'Creates a new database schema named ' + const dbLine = isInstalledWithUpgrade + ? 'Upgrades the database schema named' + : uninstallDone || installError + ? 'Creates a new database schema named' : installInProgress - ? 'Creating a new database schema named ' + ? isUpgrade + ? 'Upgrading database schema named' + : 'Creating a new database schema named' : installDone || installed || uninstallError - ? 'Created a new database schema named ' + ? 'Created a new database schema named' : uninstallInProgress - ? 'Dropping database schema named ' + ? 'Dropping database schema named' : '' - const tableAndViewLine = - uninstallDone || installError - ? 'Creates tables and views in the ' + const tableAndViewLine = isInstalledWithUpgrade + ? 'Upgrades tables and views in the' + : uninstallDone || installError + ? 'Creates tables and views in the' : installInProgress - ? 'Creating tables and views in the ' + ? isUpgrade + ? 'Upgrading tables and views in the' + : 'Creating tables and views in the' : installDone || installed || uninstallError - ? 'Created tables and views in the ' + ? 'Created tables and views in the' : uninstallInProgress - ? 'Dropping tables and views in the ' + ? 'Dropping tables and views in the' : '' - const edgeFunctionsLine = - uninstallDone || installError + const edgeFunctionsLine = isInstalledWithUpgrade + ? 'Upgrades Edge Functions to handle incoming webhooks from Stripe' + : uninstallDone || installError ? 'Deploys Edge Functions to handle incoming webhooks from Stripe' : installInProgress - ? 'Deploying Edge Functions to handle incoming webhooks from Stripe' + ? isUpgrade + ? 'Upgrading Edge Functions to handle incoming webhooks from Stripe' + : 'Deploying Edge Functions to handle incoming webhooks from Stripe' : installDone || installed || uninstallError ? 'Deployed Edge Functions to handle incoming webhooks from Stripe' : uninstallInProgress ? 'Undeploying Edge Functions to handle incoming webhooks from Stripe' : '' - const scheduleLine = - uninstallDone || installError + const scheduleLine = isInstalledWithUpgrade + ? 'Upgrades automatic Stripe data syncs using Supabase Queues' + : uninstallDone || installError ? 'Schedules automatic Stripe data syncs using Supabase Queues' : installInProgress - ? 'Scheduling automatic Stripe data syncs using Supabase Queues' + ? isUpgrade + ? 'Upgrading automatic Stripe data syncs using Supabase Queues' + : 'Scheduling automatic Stripe data syncs using Supabase Queues' : installDone || installed || uninstallError ? 'Scheduled automatic Stripe data syncs using Supabase Queues' : uninstallInProgress @@ -92,28 +111,44 @@ export const StripeSyncChangesCard = ({
  • - + {isInProgress ? ( + + ) : ( +
    + )} {dbLine} stripe
  • -
  • + {isInProgress ? ( + + ) : ( +
    + )} {tableAndViewLine} stripe schema for synced Stripe data
  • - + {isInProgress ? ( + + ) : ( + + )} {edgeFunctionsLine}
  • - + {isInProgress ? ( + + ) : ( + + )} {scheduleLine}
  • diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx index 311e61e292c..5114ef2c1a2 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx @@ -1,10 +1,9 @@ import { formatRelative } from 'date-fns' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { BadgeCheck, RefreshCwIcon, Table2 } from 'lucide-react' +import { BadgeCheck, RefreshCwIcon } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/router' -import { Button, Card, CardContent } from 'ui' -import { Admonition } from 'ui-patterns' +import { Button, Card, CardContent, CardHeader, CardTitle } from 'ui' +import { Admonition, ShimmeringLoader, TimestampInfo } from 'ui-patterns' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, @@ -19,13 +18,11 @@ import { isInstalled, isSyncRunning, isUninstalling } from './stripe-sync-status import { useStripeSyncStatus } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus' export const StripeSyncSettingsPage = () => { - const router = useRouter() const { data: project } = useSelectedProjectQuery() const { - parsedSchema: { status: installationStatus }, + schemaComment: { status: installationStatus }, syncState, - isLoading: isLoadingInstallationStatus, } = useStripeSyncStatus({ projectRef: project?.ref, connectionString: project?.connectionString, @@ -33,79 +30,92 @@ export const StripeSyncSettingsPage = () => { const installed = isInstalled(installationStatus) const isSyncing = isSyncRunning(syncState) const uninstalling = isUninstalling(installationStatus) - const tableEditorUrl = `/project/${project?.ref}/editor?schema=stripe` + + if (!installed || uninstalling) { + return ( + + + + + + ) + } return ( - {syncState && installed && !uninstalling && ( - - - - Sync Status - - - - -
    - {isSyncing ? ( - <> -
    - -
    Sync in progress...
    -
    -
    - Started{' '} - {syncState.started_at - ? formatRelative(new Date(syncState.started_at), new Date()) - : 'recently'} -
    - - ) : ( - <> -
    - -
    All up to date
    - -
    -
    - Last synced{' '} - {syncState.closed_at - ? formatRelative(new Date(syncState.closed_at), new Date()) - : 'recently'} -
    - - )} -
    -
    -
    -
    - )} - + - Stripe Schema + Manage Stripe data Access and manage the synced Stripe data in your database. - + + + + {!syncState ? ( + + ) : ( +
    + {isSyncing ? ( + <> +
    + +

    Sync in progress

    +
    + {syncState.started_at && ( +

    + Started{' '} + +

    + )} + + ) : ( + <> +
    + +

    All up to date

    +
    + {syncState.closed_at && ( +

    + Last synced{' '} + +

    + )} + + )} +
    + )} +
    +
    -
    - -
    -
    Open Stripe schema in Table Editor
    -

    - The Stripe Sync Engine stores all synced data in the{' '} - stripe schema. You can - view and query this data directly in the Table Editor. -

    -
    +
    +
    View Stripe data in Table Editor
    +

    + The Stripe Sync Engine stores all synced data in the{' '} + stripe schema. You can + view and query this data directly in the Table Editor. +

    +
    { )} /> -
    +
    } - > -
    - {/* For aria searching to target the table name instead of schema */} - {table.name} - {table.schema} - {table.name} -
    - - ) - })} - - - {fk.schema && fk.table && ( - <> - {isLoadingSelectedTable ? ( -
    - -

    Loading table columns

    -
    - ) : ( -
    - -
    -
    - {selectedSchema}.{table.name.length > 0 ? table.name : '[unnamed table]'} + updateSelectedSchema(value)} + > + {sortedSchemas.map((schema) => { + return ( + } + > +
    + {/* For aria searching to target the schema name instead of schema */} + {schema.name} + {schema.name}
    -
    - {fk.schema}.{fk.table} + + ) + })} + + + updateSelectedTable(Number(value))} + disabled={isLoadingSelectedTable} + > + + --- + + {sortBy(tables, ['schema']).map((table) => { + return ( +
    } + > +
    + {/* For aria searching to target the table name instead of schema */} + {table.name} + {table.schema} + {table.name}
    - {fk.columns.length === 0 && ( - - - There are no foreign key relations between the tables - - - )} - {fk.columns.map((_, idx) => ( - -
    - updateSelectedColumn(idx, 'source', value)} - > - + ) + })} + + + {fk.schema && fk.table && ( + <> + {isLoadingSelectedTable ? ( +
    + +

    Loading table columns

    +
    + ) : ( +
    + +
    +
    + {selectedSchema}.{table.name.length > 0 ? table.name : '[unnamed table]'} +
    +
    + {fk.schema}.{fk.table} +
    + {fk.columns.length === 0 && ( + + + There are no foreign key relations between the tables + + + )} + {fk.columns.map((_, idx) => ( + +
    + + updateSelectedColumn(idx, 'source', value) + } > - --- - - {(table?.columns ?? []) - .filter((x) => x.name.length !== 0) - .map((column) => ( + + --- + + {(table?.columns ?? []) + .filter((x) => x.name.length !== 0) + .map((column) => ( + +
    + {column.name} + + {column.format === '' ? '-' : column.format} + +
    +
    + ))} +
    +
    +
    + +
    +
    + + updateSelectedColumn(idx, 'target', value) + } + > + + --- + + {(selectedTable?.columns ?? []).map((column) => (
    {column.name} - - {column.format === '' ? '-' : column.format} - + {column.format}
    ))} -
    -
    -
    - -
    -
    - updateSelectedColumn(idx, 'target', value)} - > - - --- - - {(selectedTable?.columns ?? []).map((column) => ( - -
    - {column.name} - {column.format} -
    -
    - ))} -
    -
    -
    -
    -
    - ))} + +
    +
    +
    + + ))} +
    +
    + + {errors.columns &&

    {errors.columns}

    } + {hasTypeErrors && ( + + Column types do not match + + The following columns cannot be referenced as they are not of the same + type: + +
      + {(errors?.types ?? []).map((x, idx: number) => { + if (x === undefined) return null + return ( +
    • + {x.source} ( + {x.sourceType}) and{' '} + {x.target}( + {x.targetType}) +
    • + ) + })} +
    +
    + )} + {hasTypeNotices && ( + + Column types will be updated + + The following columns will have their types updated to match their + referenced column + +
      + {(errors?.typeNotice ?? []).map((x, idx: number) => { + if (x === undefined) return null + return ( +
    • +
      + {x.source}{' '} + {x.targetType} +
      +
    • + ) + })} +
    +
    + )} +
    -
    - - {errors.columns &&

    {errors.columns}

    } - {hasTypeErrors && ( - - Column types do not match - - The following columns cannot be referenced as they are not of the same - type: - -
      - {(errors?.types ?? []).map((x, idx: number) => { - if (x === undefined) return null - return ( -
    • - {x.source} ({x.sourceType} - ) and {x.target}( - {x.targetType}) -
    • - ) - })} -
    -
    - )} - {hasTypeNotices && ( - - Column types will be updated - - The following columns will have their types updated to match their - referenced column - -
      - {(errors?.typeNotice ?? []).map((x, idx: number) => { - if (x === undefined) return null - return ( -
    • -
      - {x.source}{' '} - {x.targetType} -
      -
    • - ) - })} -
    -
    - )} -
    - - )} + )} - {!isLoadingSelectedTable && ( - <> - + {!isLoadingSelectedTable && ( + <> + - } - title="Which action is most appropriate?" - description={ - <> -

    - The choice of the action depends on what kinds of objects the related - tables represent: -

    -
      -
    • - Cascade: if the referencing - table represents something that is a component of what is represented by - the referenced table and cannot exist independently -
    • -
    • - Restrict or{' '} - No action: if the two tables - represent independent objects -
    • -
    • - Set NULL or{' '} - Set default: if a foreign-key - relationship represents optional information -
    • -
    -

    - Typically, restricting and cascading deletes are the most common options, - but the default behavior is no action -

    - - } - url="https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK" - urlLabel="More information" - /> + } + title="Which action is most appropriate?" + description={ + <> +

    + The choice of the action depends on what kinds of objects the related + tables represent: +

    +
      +
    • + Cascade: if the referencing + table represents something that is a component of what is represented + by the referenced table and cannot exist independently +
    • +
    • + Restrict or{' '} + No action: if the two tables + represent independent objects +
    • +
    • + Set NULL or{' '} + Set default: if a + foreign-key relationship represents optional information +
    • +
    +

    + Typically, restricting and cascading deletes are the most common + options, but the default behavior is no action +

    + + } + url="https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK" + urlLabel="More information" + /> - - {generateCascadeActionDescription( - 'update', - fk.updateAction, - `${fk.schema}.${fk.table}` - )} -

    - } - onChange={(value: string) => updateCascadeAction('updateAction', value)} - > - {FOREIGN_KEY_CASCADE_OPTIONS.filter((option) => - ['no-action', 'cascade', 'restrict'].includes(option.key) - ).map((option) => ( - -

    {option.label}

    -
    - ))} -
    - - - } - descriptionText={ - <> + {generateCascadeActionDescription( - 'delete', - fk.deletionAction, + 'update', + fk.updateAction, `${fk.schema}.${fk.table}` )}

    - - } - onChange={(value: string) => updateCascadeAction('deletionAction', value)} - > - {FOREIGN_KEY_CASCADE_OPTIONS.map((option) => ( - -

    {option.label}

    -
    - ))} -
    - - )} - - )} - - - + } + onChange={(value: string) => updateCascadeAction('updateAction', value)} + > + {FOREIGN_KEY_CASCADE_OPTIONS.filter((option) => + ['no-action', 'cascade', 'restrict'].includes(option.key) + ).map((option) => ( + +

    {option.label}

    +
    + ))} +
    + + + } + descriptionText={ + <> +

    + {generateCascadeActionDescription( + 'delete', + fk.deletionAction, + `${fk.schema}.${fk.table}` + )} +

    + + } + onChange={(value: string) => updateCascadeAction('deletionAction', value)} + > + {FOREIGN_KEY_CASCADE_OPTIONS.map((option) => ( + +

    {option.label}

    +
    + ))} +
    + + )} + + )} + + + + + ) } diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.test.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.test.ts new file mode 100644 index 00000000000..6bad5f68239 --- /dev/null +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest' + +import type { ForeignKey } from './ForeignKeySelector.types' +import { + hasForeignKeySelectorChanges, + normalizeForeignKeyForDirtyCheck, +} from './ForeignKeySelector.utils' + +const BASE_FOREIGN_KEY: ForeignKey = { + id: 'fk-1', + name: 'messages_author_id_fkey', + tableId: 42, + schema: 'public', + table: 'profiles', + columns: [{ source: 'author_id', target: 'id' }], + deletionAction: 'NO ACTION', + updateAction: 'NO ACTION', +} + +describe('ForeignKeySelector dirty state', () => { + it('is clean on first open', () => { + const initialState = normalizeForeignKeyForDirtyCheck(BASE_FOREIGN_KEY) + + expect(hasForeignKeySelectorChanges(initialState, BASE_FOREIGN_KEY)).toBe(false) + }) + + it('is dirty after semantic foreign key changes', () => { + const initialState = normalizeForeignKeyForDirtyCheck(BASE_FOREIGN_KEY) + + expect( + hasForeignKeySelectorChanges(initialState, { + ...BASE_FOREIGN_KEY, + schema: 'storage', + }) + ).toBe(true) + + expect( + hasForeignKeySelectorChanges(initialState, { + ...BASE_FOREIGN_KEY, + table: 'users', + tableId: 99, + }) + ).toBe(true) + + expect( + hasForeignKeySelectorChanges(initialState, { + ...BASE_FOREIGN_KEY, + columns: [{ source: 'owner_id', target: 'id' }], + }) + ).toBe(true) + + expect( + hasForeignKeySelectorChanges(initialState, { + ...BASE_FOREIGN_KEY, + deletionAction: 'CASCADE', + }) + ).toBe(true) + + expect( + hasForeignKeySelectorChanges(initialState, { + ...BASE_FOREIGN_KEY, + updateAction: 'CASCADE', + }) + ).toBe(true) + }) + + it('ignores derived column type metadata when checking dirty state', () => { + const initialState = normalizeForeignKeyForDirtyCheck(BASE_FOREIGN_KEY) + + expect( + hasForeignKeySelectorChanges(initialState, { + ...BASE_FOREIGN_KEY, + columns: [ + { + source: 'author_id', + sourceType: 'uuid', + target: 'id', + targetType: 'uuid', + }, + ], + }) + ).toBe(false) + }) +}) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.tsx index fcb10df4f84..142aa59f8fc 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils.tsx @@ -1,10 +1,23 @@ -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' +import { FOREIGN_KEY_CASCADE_ACTION } from '@supabase/pg-meta' import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query' +import { isEqual } from 'lodash' import { HelpCircle } from 'lucide-react' import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' + import { getForeignKeyCascadeAction } from '../ColumnEditor/ColumnEditor.utils' import type { ForeignKey } from './ForeignKeySelector.types' +export interface ForeignKeyDirtyState { + id?: number | string + name?: string + tableId?: number + schema: string + table: string + columns: { source: string; target: string }[] + deletionAction: string + updateAction: string +} + export const formatForeignKeys = (fks: ForeignKeyConstraint[]): ForeignKey[] => { return fks.map((x) => { return { @@ -20,6 +33,28 @@ export const formatForeignKeys = (fks: ForeignKeyConstraint[]): ForeignKey[] => }) } +export const normalizeForeignKeyForDirtyCheck = (foreignKey: ForeignKey): ForeignKeyDirtyState => { + return { + id: foreignKey.id, + name: foreignKey.name, + tableId: foreignKey.tableId, + schema: foreignKey.schema, + table: foreignKey.table, + columns: foreignKey.columns.map(({ source, target }) => ({ source, target })), + deletionAction: foreignKey.deletionAction, + updateAction: foreignKey.updateAction, + } +} + +export const hasForeignKeySelectorChanges = ( + initialState: ForeignKeyDirtyState | undefined, + foreignKey: ForeignKey +) => { + if (initialState === undefined) return false + + return !isEqual(initialState, normalizeForeignKeyForDirtyCheck(foreignKey)) +} + export const generateCascadeActionDescription = ( action: 'update' | 'delete', cascadeAction: string, diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts index 1c09396321b..7cb3ff80328 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts @@ -1,8 +1,9 @@ +import { FOREIGN_KEY_CASCADE_ACTION } from '@supabase/pg-meta' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types' import type { ColumnField } from './SidePanelEditor.types' +import { createTable } from './SidePanelEditor.utils' // Define mock functions at module level const mockExecuteSql = vi.fn() @@ -51,9 +52,6 @@ vi.mock('components/ui/SparkBar', () => ({ default: () => null, })) -// Import after mocks are set up -import { createTable } from './SidePanelEditor.utils' - // Helper to create a column field with defaults const createColumnField = (overrides: Partial = {}): ColumnField => ({ id: 'col-1', @@ -306,7 +304,7 @@ describe('createTable', () => { const sqlCall = mockExecuteSql.mock.calls[0][0] expect(sqlCall.sql).toContain('ADD FOREIGN KEY') expect(sqlCall.sql).toContain('REFERENCES') - expect(sqlCall.sql).toContain('"users"') + expect(sqlCall.sql).toContain('users') expect(sqlCall.sql).toContain('ON DELETE CASCADE') }) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index a828d212864..e838e486e24 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -1,5 +1,16 @@ import * as Sentry from '@sentry/nextjs' -import pgMeta from '@supabase/pg-meta' +import pgMeta, { + getAddForeignKeySQL, + getAddPrimaryKeySQL, + getDropConstraintSQL, + getDuplicateIdentitySequenceSQL, + getDuplicateRowsSQL, + getDuplicateTableSQL, + getEnableRLSSQL, + getRemoveForeignKeySQL, + getUpdateIdentitySequenceSQL, + type ForeignKey, +} from '@supabase/pg-meta' import { Query } from '@supabase/pg-meta/src/query' import type { PostgresPrimaryKey } from '@supabase/postgres-meta' import type { SupaRow } from 'components/grid/types' @@ -10,7 +21,6 @@ import { deleteDatabaseColumn } from 'data/database-columns/database-column-dele import { updateDatabaseColumn } from 'data/database-columns/database-column-update-mutation' import { createDatabasePolicy } from 'data/database-policies/database-policy-create-mutation' import type { Constraint } from 'data/database/constraints-query' -import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' import { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query' import { databaseKeys } from 'data/database/keys' import { entityTypeKeys } from 'data/entity-types/keys' @@ -24,10 +34,10 @@ import { tableRowKeys } from 'data/table-rows/keys' import { executeWithRetry } from 'data/table-rows/table-rows-query' import { tableKeys } from 'data/tables/keys' import { - RetrieveTableResult, - RetrievedTableColumn, getTable, getTableQuery, + RetrievedTableColumn, + RetrieveTableResult, } from 'data/tables/table-retrieve-query' import { UpdateTableBody, @@ -45,7 +55,6 @@ import { generateCreateColumnPayload, generateUpdateColumnPayload, } from './ColumnEditor/ColumnEditor.utils' -import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types' import type { ColumnField, CreateColumnPayload, UpdateColumnPayload } from './SidePanelEditor.types' import { checkIfRelationChanged } from './TableEditor/ForeignKeysManagement/ForeignKeysManagement.utils' import type { ImportContent } from './TableEditor/TableEditor.types' @@ -81,23 +90,6 @@ export function getRowFromSidePanel( } } -/** - * The functions below are basically just queries but may be supported directly - * from the pg-meta library in the future - */ -const getAddPrimaryKeySQL = ({ - schema, - table, - columns, -}: { - schema: string - table: string - columns: string[] -}) => { - const primaryKeyColumns = columns.map((col) => `"${col}"`).join(', ') - return `ALTER TABLE "${schema}"."${table}" ADD PRIMARY KEY (${primaryKeyColumns})` -} - const addPrimaryKey = async ( projectRef: string, connectionString: string | undefined | null, @@ -121,7 +113,7 @@ const dropConstraint = async ( table: string, name: string ) => { - const query = `ALTER TABLE "${schema}"."${table}" DROP CONSTRAINT "${name}"` + const query = getDropConstraintSQL({ schema, table, name }) return await executeSql({ projectRef: projectRef, connectionString: connectionString, @@ -130,49 +122,6 @@ const dropConstraint = async ( }) } -const getAddForeignKeySQL = ({ - table, - foreignKeys, -}: { - table: { schema: string; name: string } - foreignKeys: ForeignKey[] -}) => { - const getOnDeleteSql = (action: string) => - action === FOREIGN_KEY_CASCADE_ACTION.CASCADE - ? 'ON DELETE CASCADE' - : action === FOREIGN_KEY_CASCADE_ACTION.RESTRICT - ? 'ON DELETE RESTRICT' - : action === FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT - ? 'ON DELETE SET DEFAULT' - : action === FOREIGN_KEY_CASCADE_ACTION.SET_NULL - ? 'ON DELETE SET NULL' - : '' - const getOnUpdateSql = (action: string) => - action === FOREIGN_KEY_CASCADE_ACTION.CASCADE - ? 'ON UPDATE CASCADE' - : action === FOREIGN_KEY_CASCADE_ACTION.RESTRICT - ? 'ON UPDATE RESTRICT' - : '' - return ( - foreignKeys - .map((relation) => { - const { deletionAction, updateAction } = relation - const onDeleteSql = getOnDeleteSql(deletionAction) - const onUpdateSql = getOnUpdateSql(updateAction) - return ` - ALTER TABLE "${table.schema}"."${table.name}" - ADD FOREIGN KEY (${relation.columns.map((column) => `"${column.source}"`).join(',')}) - REFERENCES "${relation.schema}"."${relation.table}" (${relation.columns.map((column) => `"${column.target}"`).join(',')}) - ${onUpdateSql} - ${onDeleteSql} - ` - .replace(/\s+/g, ' ') - .trim() - }) - .join(';') + ';' - ) -} - const addForeignKey = async ({ projectRef, connectionString, @@ -193,27 +142,6 @@ const addForeignKey = async ({ }) } -const getRemoveForeignKeySQL = ({ - table, - foreignKeys, -}: { - table: { schema: string; name: string } - foreignKeys: ForeignKey[] -}) => { - return ( - foreignKeys - .map((relation) => - ` -ALTER TABLE IF EXISTS "${table.schema}"."${table.name}" -DROP CONSTRAINT IF EXISTS "${relation.name}" -` - .replace(/\s+/g, ' ') - .trim() - ) - .join(';') + ';' - ) -} - const removeForeignKey = async ({ projectRef, connectionString, @@ -259,22 +187,6 @@ const updateForeignKey = async ({ }) } -const getUpdateIdentitySequenceSQL = ({ - schema, - table, - column, -}: { - schema: string - table: string - column: string -}) => { - return `SELECT setval('"${schema}"."${table}_${column}_seq"', (SELECT COALESCE(MAX("${column}"), 1) FROM "${schema}"."${table}"))` -} - -const getEnableRLSSQL = ({ schema, table }: { schema: string; table: string }) => { - return `ALTER TABLE "${schema}"."${table}" ENABLE ROW LEVEL SECURITY` -} - /** * The methods below involve several contexts due to the UI flow of the * dashboard and hence do not sit within their own stores @@ -350,7 +262,7 @@ export const createColumn = async ({ toast.success(`Successfully created column "${formattedPayload.name}"`, { id: toastId }) } return { error: undefined } - } catch (error: any) { + } catch (error) { toast.error(`An error occurred while creating the column "${payload.name}"`, { id: toastId }) return { error } } @@ -458,12 +370,12 @@ export const duplicateTable = async ( await executeSql({ projectRef, connectionString, - sql: [ - `CREATE TABLE "${sourceTableSchema}"."${duplicatedTableName}" (LIKE "${sourceTableSchema}"."${sourceTableName}" INCLUDING ALL);`, - payload.comment != undefined - ? `comment on table "${sourceTableSchema}"."${duplicatedTableName}" is '${payload.comment}';` - : '', - ].join('\n'), + sql: getDuplicateTableSQL({ + sourceTableName, + sourceTableSchema, + duplicatedTableName, + comment: payload.comment, + }), }) await queryClient.invalidateQueries({ queryKey: tableKeys.list(projectRef, sourceTableSchema) }) @@ -482,7 +394,11 @@ export const duplicateTable = async ( await executeSql({ projectRef, connectionString, - sql: `INSERT INTO "${sourceTableSchema}"."${duplicatedTableName}" SELECT * FROM "${sourceTableSchema}"."${sourceTableName}";`, + sql: getDuplicateRowsSQL({ + sourceTableName, + sourceTableSchema, + duplicatedTableName, + }), }) // Insert into does not copy over auto increment sequences, so we manually do it next if any @@ -492,7 +408,12 @@ export const duplicateTable = async ( await executeSql({ projectRef, connectionString, - sql: `SELECT setval('"${sourceTableSchema}"."${duplicatedTableName}_${column.name}_seq"', (SELECT MAX("${column.name}") FROM "${sourceTableSchema}"."${sourceTableName}"));`, + sql: getDuplicateIdentitySequenceSQL({ + sourceTableName, + sourceTableSchema, + duplicatedTableName, + columnName: column.name, + }), }) }) } @@ -560,7 +481,10 @@ export const createTable = async ({ // 2. Enable RLS if configured if (isRLSEnabled) { - const enableRLSSQL = getEnableRLSSQL({ schema: payload.schema, table: payload.name }) + const enableRLSSQL = getEnableRLSSQL({ + schema: payload.schema, + table: payload.name, + }) sqlStatements.push(enableRLSSQL) } diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx index b163ab1d651..183773915e7 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx @@ -21,6 +21,7 @@ import { Admonition } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' +import { useTrack } from 'lib/telemetry/track' import { defaultPrivilegesQueryOptions } from '@/data/privileges/default-privileges-query' import { useTableApiAccessQuery } from '@/data/privileges/table-api-access-query' import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' @@ -282,6 +283,7 @@ export const ApiAccessToggle = ({ isNewRecord, handler, }: ApiAccessToggleComponentProps): ReactNode => { + const track = useTrack() const isPending = handler.isPending const isError = handler.isError const isSchemaExposed = handler.data?.schemaExposed @@ -295,6 +297,13 @@ export const ApiAccessToggle = ({ if (!handler.isSuccess) return if (!isSchemaExposed) return + if (isNewRecord) { + track('table_api_access_toggle_clicked', { + newState: checked ? 'enabled' : 'disabled', + schemaName: schemaName ?? 'unknown', + }) + } + if (checked) { handler.data?.restorePreviousPrivileges() } else { diff --git a/apps/studio/components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx b/apps/studio/components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx index cffc4dd9365..db5dd46ba48 100644 --- a/apps/studio/components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx +++ b/apps/studio/components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx @@ -16,7 +16,7 @@ import { useFloatingToolbarNavSize } from './useFloatingToolbarNavSize' import { useFloatingToolbarSheet } from './useFloatingToolbarSheet' import { useFloatingToolbarSidebarClick } from './useFloatingToolbarSidebarClick' -const FloatingMobileToolbar = ({ hideMobileMenu }: { hideMobileMenu?: boolean }) => { +export const FloatingMobileToolbar = ({ hideMobileMenu }: { hideMobileMenu?: boolean }) => { const navRef = useRef(null) const sheet = useFloatingToolbarSheet(hideMobileMenu) const drag = useFloatingToolbarDrag(navRef) @@ -58,6 +58,7 @@ const FloatingMobileToolbar = ({ hideMobileMenu }: { hideMobileMenu?: boolean }) > - + - + {!!projectRef && ( <> - + - + )} {sheet.showMenuButton && sheet.isSheetOpen && ( + {showSupportCta && ( + + + + + + + Restoration taking longer than expected? + + + +

    + Restores may take from a minutes up to several hours depending on the size + of your database. However, if the restoration process is taking far longer + than expected for the size of your database, you may reach out to us via + Support for help. +

    +
    + + + +
    +
    + )} + } @@ -144,5 +182,3 @@ const RestoringState = () => { ) } - -export default RestoringState diff --git a/apps/studio/components/layouts/ProjectLayout/UpgradingState/index.tsx b/apps/studio/components/layouts/ProjectLayout/UpgradingState/index.tsx index 42ed5734c16..7a115e941a9 100644 --- a/apps/studio/components/layouts/ProjectLayout/UpgradingState/index.tsx +++ b/apps/studio/components/layouts/ProjectLayout/UpgradingState/index.tsx @@ -36,7 +36,7 @@ export const UpgradingState = () => { { projectRef: ref, projectStatus: project?.status, - trackingId: queryParams.get('trackingId'), + trackingId: queryParams?.get('trackingId'), }, { enabled: IS_PLATFORM, diff --git a/apps/studio/components/layouts/ProjectLayout/index.tsx b/apps/studio/components/layouts/ProjectLayout/index.tsx index e805fd4d9df..eff94d559a0 100644 --- a/apps/studio/components/layouts/ProjectLayout/index.tsx +++ b/apps/studio/components/layouts/ProjectLayout/index.tsx @@ -34,7 +34,7 @@ import { PausingState } from './PausingState' import { ResizingState } from './ResizingState' import RestartingState from './RestartingState' import { RestoreFailedState } from './RestoreFailedState' -import RestoringState from './RestoringState' +import { RestoringState } from './RestoringState' import { UpgradingState } from './UpgradingState' import { CreateBranchModal } from '@/components/interfaces/BranchManagement/CreateBranchModal' import { ProjectAPIDocs } from '@/components/interfaces/ProjectAPIDocs/ProjectAPIDocs' diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index 3ed0b57989c..6d99d68dcb4 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -6,20 +6,18 @@ import { PropsWithChildren, useEffect } from 'react' import { ProjectLayoutWithAuth } from '../ProjectLayout' import { SaveQueueActionBar } from '@/components/grid/components/footer/operations/SaveQueueActionBar' -import { useIsTableFilterBarEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { BannerTableEditorFilter } from '@/components/ui/BannerStack/Banners/BannerTableEditorFilter' +import { useIsQueueOperationsEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { BannerTableEditorQueueOperations } from '@/components/ui/BannerStack/Banners/BannerTableEditorQueueOperations' import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' -const TABLE_EDITOR_NEW_FILTER_BANNER_ID = 'table-editor-new-filter-banner' - export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { const { ref } = useParams() const { addBanner, dismissBanner } = useBannerStack() - const isTableFilterBarEnabled = useIsTableFilterBarEnabled() + const isTableQueueOperationsEnabled = useIsQueueOperationsEnabled() - const [isTableEditorNewFilterBannerDismissed] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''), + const [isTableEditorQueueOperationsBannerDismissed] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.TABLE_EDITOR_QUEUE_OPERATIONS_BANNER_DISMISSED(ref ?? ''), false ) @@ -28,30 +26,39 @@ export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { 'tables' ) + const { can: canWriteTables } = useAsyncCheckPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'tables' + ) + useEffect(() => { if (!isPermissionsLoaded) return - if (canReadTables && !isTableEditorNewFilterBannerDismissed && !isTableFilterBarEnabled) { + if ( + canWriteTables && + !isTableEditorQueueOperationsBannerDismissed && + !isTableQueueOperationsEnabled + ) { addBanner({ - id: TABLE_EDITOR_NEW_FILTER_BANNER_ID, + id: 'table-editor-queue-operations-banner', priority: 2, isDismissed: false, - content: , + content: , }) } else { - dismissBanner(TABLE_EDITOR_NEW_FILTER_BANNER_ID) + dismissBanner('table-editor-queue-operations-banner') } return () => { - dismissBanner(TABLE_EDITOR_NEW_FILTER_BANNER_ID) + dismissBanner('table-editor-queue-operations-banner') } }, [ addBanner, dismissBanner, - canReadTables, isPermissionsLoaded, - isTableEditorNewFilterBannerDismissed, - isTableFilterBarEnabled, + canWriteTables, + isTableEditorQueueOperationsBannerDismissed, + isTableQueueOperationsEnabled, ]) if (isPermissionsLoaded && !canReadTables) { diff --git a/apps/studio/components/ui/AlertError.tsx b/apps/studio/components/ui/AlertError.tsx index b95b2ea77aa..822c289b42c 100644 --- a/apps/studio/components/ui/AlertError.tsx +++ b/apps/studio/components/ui/AlertError.tsx @@ -8,6 +8,7 @@ import { Admonition } from 'ui-patterns/admonition' export interface AlertErrorProps { projectRef?: string subject?: string + description?: string error?: { message: string } | null layout?: 'vertical' | 'horizontal' | 'responsive' className?: string @@ -46,6 +47,7 @@ export const ContactSupportButton = ({ export const AlertError = ({ projectRef, subject, + description = 'Try refreshing your browser, but if the issue persists for more than a few minutes, please reach out to us via support.', error, className, showIcon = true, @@ -87,12 +89,7 @@ export const AlertError = ({ {formattedErrorMessage}

    )} - {showInstructions && ( -

    - Try refreshing your browser, but if the issue persists for more than a few minutes, - please reach out to us via support. -

    - )} + {showInstructions &&

    {description}

    } {children} } diff --git a/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx b/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx index e34bfd4fddb..86b27f9a729 100644 --- a/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx +++ b/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx @@ -1,7 +1,16 @@ -import { createContext, useContext, useState, useCallback } from 'react' +import { createContext, useCallback, useContext, useState } from 'react' + +const BANNER_ID = { + METRICS_API: 'metrics-api-banner', + INDEX_ADVISOR: 'index-advisor-banner', + TABLE_EDITOR_QUEUE_OPERATIONS: 'table-editor-queue-operations-banner', + RLS_EVENT_TRIGGER: 'rls-event-trigger-banner', +} as const + +export type BannerId = (typeof BANNER_ID)[keyof typeof BANNER_ID] export interface Banner { - id: string + id: BannerId content: React.ReactNode isDismissed: boolean priority?: number @@ -11,7 +20,7 @@ export interface Banner { interface BannerStackContextType { banners: Banner[] addBanner: (banner: Banner) => void - dismissBanner: (id: string) => void + dismissBanner: (id: BannerId) => void } const BannerStackContext = createContext(undefined) diff --git a/apps/studio/components/ui/BannerStack/Banners/BannerTableEditorFilter.tsx b/apps/studio/components/ui/BannerStack/Banners/BannerTableEditorFilter.tsx deleted file mode 100644 index 72be817f3e8..00000000000 --- a/apps/studio/components/ui/BannerStack/Banners/BannerTableEditorFilter.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { LOCAL_STORAGE_KEYS } from 'common' -import { useParams } from 'common/hooks' -import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -import { Search } from 'lucide-react' -import { Badge, Button } from 'ui' - -import { BannerCard } from '../BannerCard' -import { useBannerStack } from '../BannerStackProvider' -import { - useFeaturePreviewModal, - useIsTableFilterBarEnabled, -} from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' - -export const BannerTableEditorFilter = () => { - const { ref } = useParams() - const { selectFeaturePreview } = useFeaturePreviewModal() - const isTableFilterBarEnabled = useIsTableFilterBarEnabled() - - const { dismissBanner } = useBannerStack() - const [, setIsDismissed] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''), - false - ) - - const text = "name = 'John Doe'" - - return ( - { - setIsDismissed(true) - dismissBanner('table-editor-new-filter-banner') - }} - > -
    -
    - - Preview - -
    - -
    -

    - {text} -

    -
    -
    -
    -
    -

    New Table Filter Bar

    -

    - Build and modify complex filters visually -

    -
    - -
    -
    - ) -} diff --git a/apps/studio/components/ui/BannerStack/Banners/BannerTableEditorQueueOperations.tsx b/apps/studio/components/ui/BannerStack/Banners/BannerTableEditorQueueOperations.tsx new file mode 100644 index 00000000000..de2785590e1 --- /dev/null +++ b/apps/studio/components/ui/BannerStack/Banners/BannerTableEditorQueueOperations.tsx @@ -0,0 +1,75 @@ +import { LOCAL_STORAGE_KEYS } from 'common' +import { useParams } from 'common/hooks' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' +import { Badge, Button, Card, CardContent, CardHeader } from 'ui' + +import { BannerCard } from '../BannerCard' +import { useBannerStack } from '../BannerStackProvider' +import { + useFeaturePreviewModal, + useIsQueueOperationsEnabled, +} from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' + +export const BannerTableEditorQueueOperations = () => { + const { ref } = useParams() + const { selectFeaturePreview } = useFeaturePreviewModal() + const isQueueOperationsEnabled = useIsQueueOperationsEnabled() + + const { dismissBanner } = useBannerStack() + const [, setIsDismissed] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.TABLE_EDITOR_QUEUE_OPERATIONS_BANNER_DISMISSED(ref ?? ''), + false + ) + + return ( + { + setIsDismissed(true) + dismissBanner('table-editor-queue-operations-banner') + }} + > +
    +
    + + Preview + + + +
    +
    + name + · + where id = 10 +
    +
    +
    + +
    + - + Red +
    + +
    + + + Blue +
    +
    +
    +
    +
    +

    Queue row edits in Table Editor

    +

    + Batch multiple row edits and review them before saving to your database +

    +
    + +
    +
    + ) +} diff --git a/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts b/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts index de346162491..a0ce42e41b3 100644 --- a/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts +++ b/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts @@ -1,16 +1,17 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' -import { toast } from 'sonner' - import { getAccessToken } from 'common' import { databaseKeys } from 'data/database/keys' import { BASE_PATH } from 'lib/constants' import { useTrack } from 'lib/telemetry/track' +import { toast } from 'sonner' import type { ResponseError } from 'types' + import { stripeSyncKeys } from './keys' export type StripeSyncInstallVariables = { projectRef: string stripeSecretKey: string + startTime: number } export type StripeSyncInstallResponse = { @@ -21,6 +22,7 @@ export type StripeSyncInstallResponse = { export async function installStripeSync({ projectRef, stripeSecretKey, + startTime, }: StripeSyncInstallVariables): Promise { const accessToken = await getAccessToken() @@ -33,6 +35,7 @@ export async function installStripeSync({ body: JSON.stringify({ projectRef, stripeSecretKey, + startTime, }), }) diff --git a/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts b/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts index 2cd9e892bf0..ce32dcb8e85 100644 --- a/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts +++ b/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts @@ -1,15 +1,16 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' -import { toast } from 'sonner' - import { getAccessToken } from 'common' import { databaseKeys } from 'data/database/keys' import { BASE_PATH } from 'lib/constants' import { useTrack } from 'lib/telemetry/track' +import { toast } from 'sonner' import type { ResponseError } from 'types' + import { stripeSyncKeys } from './keys' export type StripeSyncUninstallVariables = { projectRef: string + startTime: number } export type StripeSyncUninstallResponse = { @@ -19,6 +20,7 @@ export type StripeSyncUninstallResponse = { export async function uninstallStripeSync({ projectRef, + startTime, }: StripeSyncUninstallVariables): Promise { const accessToken = await getAccessToken() @@ -30,6 +32,7 @@ export async function uninstallStripeSync({ }, body: JSON.stringify({ projectRef, + startTime, }), }) diff --git a/apps/studio/data/projects/project-status-query.ts b/apps/studio/data/projects/project-status-query.ts index 07beb16804d..4da0f7cc4dd 100644 --- a/apps/studio/data/projects/project-status-query.ts +++ b/apps/studio/data/projects/project-status-query.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query' - import { get, handleError } from 'data/fetchers' import type { ResponseError, UseCustomQueryOptions } from 'types' + import { projectKeys } from './keys' export type ProjectStatusVariables = { diff --git a/apps/studio/lib/api/incident-status.ts b/apps/studio/lib/api/incident-status.ts index 0ea751dcae9..670aa261a0c 100644 --- a/apps/studio/lib/api/incident-status.ts +++ b/apps/studio/lib/api/incident-status.ts @@ -1,7 +1,6 @@ -import z from 'zod' - import { IS_PLATFORM } from 'common' import { InternalServerError } from 'lib/api/apiHelpers' +import z from 'zod' export type IncidentCache = { affected_regions: Array | null @@ -81,7 +80,7 @@ export async function getActiveIncidents(): Promise { Accept: 'application/json', 'Content-Type': 'application/json', }, - cache: 'no-store', + next: { revalidate: 180 }, signal: AbortSignal.timeout(30_000), }) const responseText = await response.text() diff --git a/apps/studio/lib/validation/http-url.test.ts b/apps/studio/lib/validation/http-url.test.ts new file mode 100644 index 00000000000..c10f5a6785c --- /dev/null +++ b/apps/studio/lib/validation/http-url.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest' + +import { httpEndpointUrlSchema, isValidHttpEndpointUrl } from './http-url' + +const schema = httpEndpointUrlSchema({ + requiredMessage: 'required', + invalidMessage: 'invalid', + prefixMessage: 'prefix', +}) + +describe('isValidHttpEndpointUrl', () => { + it('accepts valid http and https endpoints', () => { + expect(isValidHttpEndpointUrl('https://api.supabase.com/webhooks')).toBe(true) + expect(isValidHttpEndpointUrl('http://localhost:3000/hooks')).toBe(true) + expect(isValidHttpEndpointUrl('https://127.0.0.1:4318/v1/logs')).toBe(true) + expect(isValidHttpEndpointUrl('https://[::1]:4318/v1/logs')).toBe(true) + }) + + it('rejects invalid endpoint URLs', () => { + expect(isValidHttpEndpointUrl('https://webhook')).toBe(false) + expect(isValidHttpEndpointUrl('ftp://api.supabase.com/webhooks')).toBe(false) + expect(isValidHttpEndpointUrl('not a url')).toBe(false) + }) +}) + +describe('httpEndpointUrlSchema', () => { + it('rejects empty values', () => { + const result = schema.safeParse('') + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.issues[0].message).toBe('required') + } + }) + + it('rejects URLs without an http or https prefix', () => { + const result = schema.safeParse('api.supabase.com/webhooks') + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.issues[0].message).toBe('prefix') + } + }) + + it('rejects incomplete hostnames', () => { + const result = schema.safeParse('https://webhook') + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.issues[0].message).toBe('invalid') + } + }) + + it('accepts valid endpoints after trimming', () => { + const result = schema.safeParse(' https://api.supabase.com/webhooks ') + + expect(result.success).toBe(true) + if (result.success) { + expect(result.data).toBe('https://api.supabase.com/webhooks') + } + }) +}) diff --git a/apps/studio/lib/validation/http-url.ts b/apps/studio/lib/validation/http-url.ts new file mode 100644 index 00000000000..a6bd0c85074 --- /dev/null +++ b/apps/studio/lib/validation/http-url.ts @@ -0,0 +1,59 @@ +import { z } from 'zod' + +const HTTP_URL_PROTOCOL_REGEX = /^https?:\/\// +const IPV4_SEGMENT = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)' +const IPV4_REGEX = new RegExp(`^(?:${IPV4_SEGMENT}\\.){3}${IPV4_SEGMENT}$`) +const BRACKETED_IPV6_REGEX = /^\[[0-9a-f:.]+\]$/i + +export const hasHttpUrlProtocol = (value: string) => HTTP_URL_PROTOCOL_REGEX.test(value) + +export const isValidHttpEndpointUrl = (value: string) => { + try { + const url = new URL(value) + if (url.protocol !== 'http:' && url.protocol !== 'https:') return false + + const { hostname } = url + return ( + hostname === 'localhost' || + hostname.includes('.') || + IPV4_REGEX.test(hostname) || + BRACKETED_IPV6_REGEX.test(hostname) + ) + } catch { + return false + } +} + +type HttpEndpointUrlSchemaOptions = { + requiredMessage: string + invalidMessage: string + prefixMessage: string +} + +export const httpEndpointUrlSchema = ({ + requiredMessage, + invalidMessage, + prefixMessage, +}: HttpEndpointUrlSchemaOptions) => + z + .string() + .trim() + .min(1, requiredMessage) + .superRefine((value, ctx) => { + if (!value) return + + if (!hasHttpUrlProtocol(value)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: prefixMessage, + }) + return + } + + if (!isValidHttpEndpointUrl(value)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: invalidMessage, + }) + } + }) diff --git a/apps/studio/package.json b/apps/studio/package.json index caf3c5239d9..36d3624cf1f 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -103,7 +103,7 @@ "memoize-one": "^5.0.1", "mime-db": "^1.53.0", "monaco-editor": "0.52.2", - "next": "~16.1.6", + "next": "~16.1.7", "next-themes": "^0.3.0", "nuqs": "2.7.1", "openai": "^4.75.1", @@ -137,7 +137,7 @@ "sql-formatter": "^15.0.0", "sqlstring": "^2.3.2", "streamdown": "^1.3.0", - "stripe-experiment-sync": "^1.0.19", + "stripe-experiment-sync": "1.0.27", "tus-js-client": "^4.1.0", "ui": "workspace:*", "ui-patterns": "workspace:*", diff --git a/apps/studio/pages/api/integrations/stripe-sync.ts b/apps/studio/pages/api/integrations/stripe-sync.ts index 3e43dd7de11..c55cd743ee8 100644 --- a/apps/studio/pages/api/integrations/stripe-sync.ts +++ b/apps/studio/pages/api/integrations/stripe-sync.ts @@ -7,10 +7,12 @@ import { z } from 'zod' const InstallBodySchema = z.object({ projectRef: z.string().min(1), stripeSecretKey: z.string().min(1), + startTime: z.number().positive().optional(), }) const UninstallBodySchema = z.object({ projectRef: z.string().min(1), + startTime: z.number().positive().optional(), }) async function isStripeSyncEnabled() { @@ -26,6 +28,10 @@ function getBearerToken(req: NextApiRequest) { return match?.[1]?.trim() ?? null } +export const config = { + maxDuration: 300, // 5 minutes, since the installation process can take a while even if happening in background +} + export default async function handler(req: NextApiRequest, res: NextApiResponse) { // Hide endpoint if the integration is disabled. if (!(await isStripeSyncEnabled())) { @@ -59,7 +65,7 @@ async function handleDeleteStripeSyncInstall(req: NextApiRequest, res: NextApiRe .status(400) .json({ data: null, error: { message: 'Bad Request: Invalid request body' } }) } - const { projectRef } = parsed.data + const { projectRef, startTime } = parsed.data waitUntil( uninstall({ @@ -67,6 +73,7 @@ async function handleDeleteStripeSyncInstall(req: NextApiRequest, res: NextApiRe supabaseProjectRef: projectRef, baseProjectUrl: process.env.NEXT_PUBLIC_CUSTOMER_DOMAIN, supabaseManagementUrl: process.env.NEXT_PUBLIC_API_DOMAIN, + startTime, }).catch((error) => { console.error('Stripe Sync Engine uninstallation failed.', error) throw error @@ -92,7 +99,7 @@ async function handleSetupStripeSyncInstall(req: NextApiRequest, res: NextApiRes .status(400) .json({ data: null, error: { message: 'Bad Request: Invalid request body' } }) } - const { projectRef, stripeSecretKey } = parsed.data + const { projectRef, stripeSecretKey, startTime } = parsed.data // Validate the Stripe API key before proceeding with installation try { @@ -113,10 +120,12 @@ async function handleSetupStripeSyncInstall(req: NextApiRequest, res: NextApiRes error: { message: errorMessage }, }) } - } catch (error: any) { + } catch (error) { + const normalizedErrorMessage = error instanceof Error ? error.message : String(error) + return res.status(400).json({ data: null, - error: { message: `Failed to validate Stripe API key: ${error.message}` }, + error: { message: `Failed to validate Stripe API key: ${normalizedErrorMessage}` }, }) } waitUntil( @@ -127,6 +136,7 @@ async function handleSetupStripeSyncInstall(req: NextApiRequest, res: NextApiRes baseProjectUrl: process.env.NEXT_PUBLIC_CUSTOMER_DOMAIN, supabaseManagementUrl: process.env.NEXT_PUBLIC_API_DOMAIN, packageVersion: VERSION, + startTime, }).catch((error) => { console.error('Stripe Sync Engine installation failed.', error) throw error diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index e6d8b3192ab..cefe4ac1267 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -46,6 +46,7 @@ import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useDataApiGrantTogglesEnabled } from 'hooks/misc/useDataApiGrantTogglesEnabled' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { usePHFlag } from 'hooks/ui/useFlag' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { withAuth } from 'hooks/misc/withAuth' @@ -96,6 +97,10 @@ const Wizard: NextPageWithLayout = () => { const showPostgresVersionSelector = useFlag('showPostgresVersionSelector') const cloudProviderEnabled = useFlag('enableFlyCloudProvider') const isDataApiGrantTogglesEnabled = useDataApiGrantTogglesEnabled() + // Read the raw flag for telemetry — useDataApiGrantTogglesEnabled coerces undefined→false, + // which would record false for users whose flags haven't loaded yet. The raw value preserves + // undefined (omitted from PostHog) so we only record true/false when the flag is resolved. + const tableEditorApiAccessToggleFlag = usePHFlag('tableEditorApiAccessToggle') const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' const isNotOnHigherPlan = !['team', 'enterprise', 'platform'].includes(currentOrg?.plan.id ?? '') @@ -259,6 +264,9 @@ const Wizard: NextPageWithLayout = () => { enableRlsEventTrigger: form.getValues('enableRlsEventTrigger'), dataApiEnabled: form.getValues('dataApi'), useOrioleDb: form.getValues('useOrioleDb'), + ...(tableEditorApiAccessToggleFlag !== undefined && { + tableEditorApiAccessToggleEnabled: tableEditorApiAccessToggleFlag, + }), }, { project: res.ref, diff --git a/apps/studio/pages/project/[ref]/observability/query-performance.tsx b/apps/studio/pages/project/[ref]/observability/query-performance.tsx index 62b4a4a137a..f33e60bbf9f 100644 --- a/apps/studio/pages/project/[ref]/observability/query-performance.tsx +++ b/apps/studio/pages/project/[ref]/observability/query-performance.tsx @@ -1,20 +1,20 @@ -import { parseAsArrayOf, parseAsInteger, parseAsJson, parseAsString, useQueryStates } from 'nuqs' -import { NumericFilter } from 'components/interfaces/Reports/v2/ReportsNumericFilter' - import { useParams } from 'common' import { useIndexAdvisorStatus } from 'components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus' import { useQueryPerformanceSort } from 'components/interfaces/QueryPerformance/hooks/useQueryPerformanceSort' import { QueryPerformance } from 'components/interfaces/QueryPerformance/QueryPerformance' +import { type QuerySource } from 'components/interfaces/QueryPerformance/QueryPerformance.types' +import { useQueryPerformanceQuery } from 'components/interfaces/QueryPerformance/useQueryPerformanceQuery' import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants' -import { useQueryPerformanceQuery } from 'components/interfaces/Reports/Reports.queries' import { Presets } from 'components/interfaces/Reports/Reports.types' import { queriesFactory } from 'components/interfaces/Reports/Reports.utils' +import { NumericFilter } from 'components/interfaces/Reports/v2/ReportsNumericFilter' import { DefaultLayout } from 'components/layouts/DefaultLayout' import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout' import { DatabaseSelector } from 'components/ui/DatabaseSelector' import { DocsButton } from 'components/ui/DocsButton' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import { parseAsArrayOf, parseAsInteger, parseAsJson, parseAsString, useQueryStates } from 'nuqs' import type { NextPageWithLayout } from 'types' import { Admonition } from 'ui-patterns' @@ -25,12 +25,20 @@ const QueryPerformanceReport: NextPageWithLayout = () => { const { sort: sortConfig } = useQueryPerformanceSort() const [ - { search: searchQuery, roles, minCalls, totalTimeFilter: totalTimeFilterRaw, indexAdvisor }, + { + search: searchQuery, + roles, + sources, + minCalls, + totalTimeFilter: totalTimeFilterRaw, + indexAdvisor, + }, ] = useQueryStates({ sort: parseAsString, order: parseAsString, search: parseAsString.withDefault(''), roles: parseAsArrayOf(parseAsString).withDefault([]), + sources: parseAsArrayOf(parseAsString).withDefault([]), minCalls: parseAsInteger, totalTimeFilter: parseAsJson((value) => value === null || value === undefined ? null : (value as NumericFilter) @@ -57,6 +65,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => { orderBy: sortConfig || undefined, preset: 'unified', roles, + sources: sources as QuerySource[], runIndexAdvisor: isIndexAdvisorEnabled, minCalls: minCalls ?? undefined, minTotalTime, diff --git a/apps/studio/pages/project/[ref]/sql/quickstarts.tsx b/apps/studio/pages/project/[ref]/sql/quickstarts.tsx index 2b69bbc2512..ea7b3b34d43 100644 --- a/apps/studio/pages/project/[ref]/sql/quickstarts.tsx +++ b/apps/studio/pages/project/[ref]/sql/quickstarts.tsx @@ -12,7 +12,7 @@ import type { NextPageWithLayout } from 'types' const SqlQuickstarts: NextPageWithLayout = () => { const router = useRouter() - const { ref } = useParams<{ ref: string }>() + const ref = useParams<{ ref: string }>()?.ref const tabs = useTabsStateSnapshot() useEffect(() => { diff --git a/apps/studio/pages/project/[ref]/sql/templates.tsx b/apps/studio/pages/project/[ref]/sql/templates.tsx index 2e219edaa52..c1e2db63994 100644 --- a/apps/studio/pages/project/[ref]/sql/templates.tsx +++ b/apps/studio/pages/project/[ref]/sql/templates.tsx @@ -12,7 +12,7 @@ import type { NextPageWithLayout } from 'types' const SqlTemplates: NextPageWithLayout = () => { const router = useRouter() - const { ref } = useParams<{ ref: string }>() + const ref = useParams<{ ref: string }>()?.ref const tabs = useTabsStateSnapshot() useEffect(() => { diff --git a/apps/studio/tsconfig.json b/apps/studio/tsconfig.json index 4ae881822a2..f6061f9781c 100644 --- a/apps/studio/tsconfig.json +++ b/apps/studio/tsconfig.json @@ -36,7 +36,8 @@ "**/*.js", "**/*.jsx", ".next/types/**/*.ts", - "./../../packages/ui/src/**/*.d.ts" + "./../../packages/ui/src/**/*.d.ts", + ".next/dev/types/**/*.ts" ], "exclude": ["node_modules", "public/deno/*.ts"] } diff --git a/apps/www/data/career.json b/apps/www/data/career.json index 897553de4b6..7d1672ab0f4 100644 --- a/apps/www/data/career.json +++ b/apps/www/data/career.json @@ -1,19 +1,19 @@ { "company": [ { - "number": "180+", - "text": "team members\n\nin 35+ countries" + "number": "280+", + "text": "team members\n\nin 55+ countries" }, { - "number": "15+", + "number": "20+", "text": "languages spoken" }, { - "number": "$496M", - "text": "in funding" + "number": "$500M", + "text": "raised" }, { - "number": "430,000+", + "number": "540,000+", "text": "community members" }, { @@ -25,27 +25,27 @@ { "icon": "egoless", "title": "Egoless", - "text": "Willing to do the schlep, the team goals come first." + "text": "Mission driven, work for the good of colleagues." }, { - "icon": "curious", - "title": "Polyglot", - "text": "Broad skillsets. Growth mindset." - }, - { - "icon": "process", - "title": "Process Driven", - "text": "Engineer yourself out of the job." + "icon": "kaizen", + "title": "Kaizen Mindset", + "text": "Iterative approach, continuously improve all functions." }, { "icon": "honesty", - "title": "Intellectual Honesty", - "text": "Put our own biases aside." + "title": "Truth Seeking", + "text": "Intellectually honest, act with integrity, recognize biases." }, { - "icon": "flexibility", - "title": "Kaizen mindset", - "text": "Continuously improve all functions." + "icon": "batteries", + "title": "Batteries Included", + "text": "High ownership, intrinsically motivated, care about quality." + }, + { + "icon": "undeniable", + "title": "Undeniable", + "text": "So good you earn respect, reliable, consistently deliver." } ], "contributors": [ diff --git a/docker/.env.example b/docker/.env.example index 2bcf16edbd0..81b7c8e9c47 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -299,7 +299,7 @@ SERVICE_ROLE_KEY_ASYMMETRIC= ############ # Enable webp support -IMGPROXY_ENABLE_WEBP_DETECTION=true +IMGPROXY_AUTO_WEBP=true ############ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 0f0e7058edf..6a026b87607 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -394,7 +394,7 @@ services: IMGPROXY_BIND: ":5001" IMGPROXY_LOCAL_FILESYSTEM_ROOT: / IMGPROXY_USE_ETAG: "true" - IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} + IMGPROXY_AUTO_WEBP: ${IMGPROXY_AUTO_WEBP} IMGPROXY_MAX_SRC_RESOLUTION: 16.8 meta: @@ -600,7 +600,8 @@ services: ] interval: 10s timeout: 5s - retries: 5 + retries: 10 + start_period: 30s depends_on: db: condition: service_healthy diff --git a/e2e/studio/utils/test.ts b/e2e/studio/utils/test.ts index 4dd361ebd1e..05263367897 100644 --- a/e2e/studio/utils/test.ts +++ b/e2e/studio/utils/test.ts @@ -22,7 +22,10 @@ export const test = base.extend({ page: async ({ page }, use) => { const ref = env.PROJECT_REF ?? 'default' await page.addInitScript((ref) => { - localStorage.setItem(`table-editor-new-filter-banner-dismissed-${ref}`, JSON.stringify(true)) + localStorage.setItem( + `table-editor-queue-operations-banner-dismissed-${ref}`, + JSON.stringify(true) + ) }, ref) await use(page) }, diff --git a/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts b/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts index a1cfbb223e0..b9d143ebbce 100644 --- a/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts +++ b/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts @@ -1,90 +1,82 @@ // AWS SDK issue: https://github.com/aws/aws-sdk-js-v3/issues/6134 // We need to mock the file system for the AWS SDK to work. -import { - prepareVirtualFile, -} from "https://deno.land/x/mock_file@v1.1.2/mod.ts"; +import { prepareVirtualFile } from 'https://deno.land/x/mock_file@v1.1.2/mod.ts' -import { - BedrockRuntimeClient, - InvokeModelCommand, -} from "npm:@aws-sdk/client-bedrock-runtime"; -import { createClient } from "npm:@supabase/supabase-js"; -import { decode } from "npm:base64-arraybuffer"; +import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime' +import { createClient } from 'npm:@supabase/supabase-js' +import { decode } from 'npm:base64-arraybuffer' -console.log("Hello from Amazon Bedrock!"); +console.log('Hello from Amazon Bedrock!') Deno.serve(async (req) => { - prepareVirtualFile("./aws/config"); - prepareVirtualFile("./aws/credentials"); + prepareVirtualFile('./aws/config') + prepareVirtualFile('./aws/credentials') const client = new BedrockRuntimeClient({ - region: "us-west-2", + region: 'us-west-2', credentials: { - accessKeyId: Deno.env.get("AWS_ACCESS_KEY_ID") ?? "", - secretAccessKey: Deno.env.get("AWS_SECRET_ACCESS_KEY") ?? "", - sessionToken: Deno.env.get("AWS_SESSION_TOKEN") ?? "", + accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '', + secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '', + sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '', }, - }); + }) - const { prompt, seed } = await req.json(); - console.log(prompt); + const { prompt, seed } = await req.json() + console.log(prompt) const input = { - contentType: "application/json", - accept: "*/*", - modelId: "amazon.titan-image-generator-v1", + contentType: 'application/json', + accept: '*/*', + modelId: 'amazon.titan-image-generator-v1', body: JSON.stringify({ - "taskType": "TEXT_IMAGE", - "textToImageParams": { "text": prompt }, - "imageGenerationConfig": { - "numberOfImages": 1, - "quality": "standard", - "cfgScale": 8.0, - "height": 512, - "width": 512, - "seed": seed ?? 0, + taskType: 'TEXT_IMAGE', + textToImageParams: { text: prompt }, + imageGenerationConfig: { + numberOfImages: 1, + quality: 'standard', + cfgScale: 8.0, + height: 512, + width: 512, + seed: seed ?? 0, }, }), - }; + } - const command = new InvokeModelCommand(input); - const response = await client.send(command); - console.log(response); + const command = new InvokeModelCommand(input) + const response = await client.send(command) + console.log(response) if (response.$metadata.httpStatusCode === 200) { - const { body, $metadata } = response; + const { body, $metadata } = response - const textDecoder = new TextDecoder("utf-8"); - const jsonString = textDecoder.decode(body.buffer); - const parsedData = JSON.parse(jsonString); - console.log(parsedData); - const image = parsedData.images[0]; + const textDecoder = new TextDecoder('utf-8') + const jsonString = textDecoder.decode(body.buffer) + const parsedData = JSON.parse(jsonString) + console.log(parsedData) + const image = parsedData.images[0] const supabaseClient = createClient( // Supabase API URL - env var exported by default. - Deno.env.get("SUPABASE_URL")!, + Deno.env.get('SUPABASE_URL')!, // Supabase API ANON KEY - env var exported by default. - Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, - ); + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! + ) const { data: upload, error: uploadError } = await supabaseClient.storage - .from("images") - .upload(`${$metadata.requestId ?? ""}.png`, decode(image), { - contentType: "image/png", - cacheControl: "3600", + .from('images') + .upload(`${$metadata.requestId ?? ''}.png`, decode(image), { + contentType: 'image/png', + cacheControl: '3600', upsert: false, - }); + }) if (!upload) { - return Response.json(uploadError); + return Response.json(uploadError) } - const { data } = supabaseClient - .storage - .from("images") - .getPublicUrl(upload.path!); - return Response.json(data); + const { data } = supabaseClient.storage.from('images').getPublicUrl(upload.path!) + return Response.json(data) } - return Response.json(response); -}); + return Response.json(response) +}) /* To invoke locally: diff --git a/examples/ai/edge-functions/supabase/functions/_shared/database.types.ts b/examples/ai/edge-functions/supabase/functions/_shared/database.types.ts index 95636805395..ba734f0b07b 100644 --- a/examples/ai/edge-functions/supabase/functions/_shared/database.types.ts +++ b/examples/ai/edge-functions/supabase/functions/_shared/database.types.ts @@ -1,10 +1,4 @@ -export type Json = - | string - | number - | boolean - | null - | { [key: string]: Json | undefined } - | Json[] +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] export type Database = { graphql_public: { @@ -180,11 +174,11 @@ export type Database = { } Relationships: [ { - foreignKeyName: "objects_bucketId_fkey" - columns: ["bucket_id"] + foreignKeyName: 'objects_bucketId_fkey' + columns: ['bucket_id'] isOneToOne: false - referencedRelation: "buckets" - referencedColumns: ["id"] + referencedRelation: 'buckets' + referencedColumns: ['id'] }, ] } @@ -257,27 +251,25 @@ export type Database = { } } -type PublicSchema = Database[Extract] +type PublicSchema = Database[Extract] export type Tables< PublicTableNameOrOptions extends - | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) + | keyof (PublicSchema['Tables'] & PublicSchema['Views']) | { schema: keyof Database }, TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & - Database[PublicTableNameOrOptions["schema"]]["Views"]) + ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views']) : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } - ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & - Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { + ? (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends { Row: infer R } ? R : never - : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & - PublicSchema["Views"]) - ? (PublicSchema["Tables"] & - PublicSchema["Views"])[PublicTableNameOrOptions] extends { + : PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & PublicSchema['Views']) + ? (PublicSchema['Tables'] & PublicSchema['Views'])[PublicTableNameOrOptions] extends { Row: infer R } ? R @@ -285,20 +277,18 @@ export type Tables< : never export type TablesInsert< - PublicTableNameOrOptions extends - | keyof PublicSchema["Tables"] - | { schema: keyof Database }, + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { Insert: infer I } ? I : never - : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] - ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { Insert: infer I } ? I @@ -306,20 +296,18 @@ export type TablesInsert< : never export type TablesUpdate< - PublicTableNameOrOptions extends - | keyof PublicSchema["Tables"] - | { schema: keyof Database }, + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { Update: infer U } ? U : never - : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] - ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { Update: infer U } ? U @@ -327,14 +315,12 @@ export type TablesUpdate< : never export type Enums< - PublicEnumNameOrOptions extends - | keyof PublicSchema["Enums"] - | { schema: keyof Database }, + PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] | { schema: keyof Database }, EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] + ? keyof Database[PublicEnumNameOrOptions['schema']]['Enums'] : never = never, > = PublicEnumNameOrOptions extends { schema: keyof Database } - ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] - : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] - ? PublicSchema["Enums"][PublicEnumNameOrOptions] + ? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] + ? PublicSchema['Enums'][PublicEnumNameOrOptions] : never diff --git a/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts b/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts index 89174e016c4..71fec0bd761 100644 --- a/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts +++ b/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts @@ -1,47 +1,47 @@ -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; +import 'jsr:@supabase/functions-js/edge-runtime.d.ts' -import { createClient } from "jsr:@supabase/supabase-js@2"; -import { Database, Tables } from "../_shared/database.types.ts"; +import { createClient } from 'jsr:@supabase/supabase-js@2' +import { Database, Tables } from '../_shared/database.types.ts' -type EmbeddingsRecord = Tables<"embeddings">; +type EmbeddingsRecord = Tables<'embeddings'> interface WebhookPayload { - type: "INSERT" | "UPDATE" | "DELETE"; - table: string; - record: EmbeddingsRecord; - schema: "public"; - old_record: null | EmbeddingsRecord; + type: 'INSERT' | 'UPDATE' | 'DELETE' + table: string + record: EmbeddingsRecord + schema: 'public' + old_record: null | EmbeddingsRecord } const supabase = createClient( - Deno.env.get("SUPABASE_URL")!, - Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, -); + Deno.env.get('SUPABASE_URL')!, + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! +) -const model = new Supabase.ai.Session("gte-small"); +const model = new Supabase.ai.Session('gte-small') Deno.serve(async (req) => { - const payload: WebhookPayload = await req.json(); - const { content, id } = payload.record; + const payload: WebhookPayload = await req.json() + const { content, id } = payload.record // Check if content has changed. if (content === payload?.old_record?.content) { - return new Response("ok - no change"); + return new Response('ok - no change') } // Generate embedding const embedding = await model.run(content, { mean_pool: true, normalize: true, - }); + }) // Store in DB - const { error } = await supabase.from("embeddings").update({ - embedding: JSON.stringify(embedding), - }).eq( - "id", - id, - ); - if (error) console.warn(error.message); + const { error } = await supabase + .from('embeddings') + .update({ + embedding: JSON.stringify(embedding), + }) + .eq('id', id) + if (error) console.warn(error.message) - return new Response("ok - updated"); -}); + return new Response('ok - updated') +}) diff --git a/examples/ai/edge-functions/supabase/functions/search/index.ts b/examples/ai/edge-functions/supabase/functions/search/index.ts index 2f045d7a0aa..3e7c7b0ce30 100644 --- a/examples/ai/edge-functions/supabase/functions/search/index.ts +++ b/examples/ai/edge-functions/supabase/functions/search/index.ts @@ -1,38 +1,38 @@ -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; +import 'jsr:@supabase/functions-js/edge-runtime.d.ts' -import { createClient } from "jsr:@supabase/supabase-js@2"; -import { Database } from "../_shared/database.types.ts"; +import { createClient } from 'jsr:@supabase/supabase-js@2' +import { Database } from '../_shared/database.types.ts' const supabase = createClient( - Deno.env.get("SUPABASE_URL")!, - Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, -); + Deno.env.get('SUPABASE_URL')!, + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! +) -const model = new Supabase.ai.Session("gte-small"); +const model = new Supabase.ai.Session('gte-small') Deno.serve(async (req) => { - const { search } = await req.json(); - if (!search) return new Response("Please provide a search param!"); + const { search } = await req.json() + if (!search) return new Response('Please provide a search param!') // Generate embedding for search term. const embedding = await model.run(search, { mean_pool: true, normalize: true, - }); + }) // Query embeddings. const { data: result, error } = await supabase - .rpc("query_embeddings", { + .rpc('query_embeddings', { embedding: JSON.stringify(embedding), match_threshold: 0.8, }) - .select("content") - .limit(3); + .select('content') + .limit(3) if (error) { - return Response.json(error); + return Response.json(error) } - return Response.json({ search, result }); -}); + return Response.json({ search, result }) +}) /* To invoke locally: diff --git a/examples/ai/llamafile-edge/supabase/functions/llamafile-stream/index.ts b/examples/ai/llamafile-edge/supabase/functions/llamafile-stream/index.ts index b3b78329f7d..538691f0aa6 100644 --- a/examples/ai/llamafile-edge/supabase/functions/llamafile-stream/index.ts +++ b/examples/ai/llamafile-edge/supabase/functions/llamafile-stream/index.ts @@ -1,59 +1,59 @@ // https://github.com/Mozilla-Ocho/llamafile?tab=readme-ov-file#quickstart -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; -const session = new Supabase.ai.Session("LLaMA_CPP"); +import 'jsr:@supabase/functions-js/edge-runtime.d.ts' +const session = new Supabase.ai.Session('LLaMA_CPP') Deno.serve(async (req: Request) => { - const params = new URL(req.url).searchParams; - const prompt = params.get("prompt") ?? ""; + const params = new URL(req.url).searchParams + const prompt = params.get('prompt') ?? '' // Get the output as a stream - const output = await session.run({ - messages: [ - { - "role": "system", - "content": - "You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.", - }, - { - "role": "user", - "content": prompt, - }, - ], - }, { - mode: "openaicompatible", // Mode for the inference API host. (default: 'ollama') - stream: true, - }) as AsyncGenerator; + const output = (await session.run( + { + messages: [ + { + role: 'system', + content: + 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.', + }, + { + role: 'user', + content: prompt, + }, + ], + }, + { + mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama') + stream: true, + } + )) as AsyncGenerator const body = new ReadableStream({ async pull(ctrl) { try { - const item = await output.next(); + const item = await output.next() if (item.done) { - console.log("done"); - ctrl.close(); - return; + console.log('done') + ctrl.close() + return } - ctrl.enqueue("data: "); - ctrl.enqueue(JSON.stringify(item.value)); - ctrl.enqueue("\r\n\r\n"); + ctrl.enqueue('data: ') + ctrl.enqueue(JSON.stringify(item.value)) + ctrl.enqueue('\r\n\r\n') } catch (err) { - console.error(err); - ctrl.close(); + console.error(err) + ctrl.close() } }, - }); + }) - return new Response( - body.pipeThrough(new TextEncoderStream()), - { - headers: { - "Content-Type": "text/event-stream", - }, + return new Response(body.pipeThrough(new TextEncoderStream()), { + headers: { + 'Content-Type': 'text/event-stream', }, - ); -}); + }) +}) /** Run locally: diff --git a/examples/ai/llamafile-edge/supabase/functions/llamafile/index.ts b/examples/ai/llamafile-edge/supabase/functions/llamafile/index.ts index 48dfe82cab3..fa6d03e416e 100644 --- a/examples/ai/llamafile-edge/supabase/functions/llamafile/index.ts +++ b/examples/ai/llamafile-edge/supabase/functions/llamafile/index.ts @@ -1,32 +1,35 @@ // https://github.com/Mozilla-Ocho/llamafile?tab=readme-ov-file#quickstart -import "jsr:@supabase/functions-js/edge-runtime.d.ts"; -const session = new Supabase.ai.Session("LLaMA_CPP"); +import 'jsr:@supabase/functions-js/edge-runtime.d.ts' +const session = new Supabase.ai.Session('LLaMA_CPP') Deno.serve(async (req: Request) => { - const params = new URL(req.url).searchParams; - const prompt = params.get("prompt") ?? ""; + const params = new URL(req.url).searchParams + const prompt = params.get('prompt') ?? '' // Get the output as a stream - const output = await session.run({ - messages: [ - { - "role": "system", - "content": - "You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.", - }, - { - "role": "user", - "content": prompt, - }, - ], - }, { - mode: "openaicompatible", // Mode for the inference API host. (default: 'ollama') - stream: false, - }); + const output = await session.run( + { + messages: [ + { + role: 'system', + content: + 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.', + }, + { + role: 'user', + content: prompt, + }, + ], + }, + { + mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama') + stream: false, + } + ) - console.log("done"); - return Response.json(output); -}); + console.log('done') + return Response.json(output) +}) /** Run locally: diff --git a/examples/ai/llamafile-edge/supabase/functions/openai-sdk/index.ts b/examples/ai/llamafile-edge/supabase/functions/openai-sdk/index.ts index 8411c6d5c1c..1dee961ec67 100644 --- a/examples/ai/llamafile-edge/supabase/functions/openai-sdk/index.ts +++ b/examples/ai/llamafile-edge/supabase/functions/openai-sdk/index.ts @@ -3,66 +3,64 @@ // This enables autocomplete, go to definition, etc. // Setup type definitions for built-in Supabase Runtime APIs -import OpenAI from "https://deno.land/x/openai@v4.53.2/mod.ts"; +import OpenAI from 'https://deno.land/x/openai@v4.53.2/mod.ts' -console.log("Hello from openai-sdk compatible!"); +console.log('Hello from openai-sdk compatible!') Deno.serve(async (req) => { - const client = new OpenAI(); - const { prompt } = await req.json(); - const stream = true; + const client = new OpenAI() + const { prompt } = await req.json() + const stream = true const chatCompletion = await client.chat.completions.create({ - model: "LLaMA_CPP", + model: 'LLaMA_CPP', stream, messages: [ { - role: "system", + role: 'system', content: - "You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.", + 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.', }, { - role: "user", + role: 'user', content: prompt, }, ], - }); + }) if (stream) { const headers = new Headers({ - "Content-Type": "text/event-stream", - Connection: "keep-alive", - }); + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + }) // Create a stream const stream = new ReadableStream({ async start(controller) { - const encoder = new TextEncoder(); + const encoder = new TextEncoder() try { for await (const part of chatCompletion) { - controller.enqueue( - encoder.encode(part.choices[0]?.delta?.content || ""), - ); + controller.enqueue(encoder.encode(part.choices[0]?.delta?.content || '')) } } catch (err) { - console.error("Stream error:", err); + console.error('Stream error:', err) } finally { - controller.close(); + controller.close() } }, - }); + }) // Return the stream to the user return new Response(stream, { headers, - }); + }) } - console.log(chatCompletion); + console.log(chatCompletion) - return Response.json(chatCompletion); -}); + return Response.json(chatCompletion) +}) /* To invoke locally: diff --git a/examples/auth/expo-social-auth/app.json b/examples/auth/expo-social-auth/app.json index 53e6a620c34..b6182604b63 100644 --- a/examples/auth/expo-social-auth/app.json +++ b/examples/auth/expo-social-auth/app.json @@ -51,4 +51,4 @@ "reactCompiler": true } } -} \ No newline at end of file +} diff --git a/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx b/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx index 34b7acd92bd..4fe575d9ab8 100644 --- a/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx +++ b/examples/auth/expo-social-auth/app/(tabs)/_layout.tsx @@ -1,17 +1,18 @@ -import { Tabs } from 'expo-router'; +import { Tabs } from 'expo-router' -import { Colors } from '@/constants/theme'; -import { useColorScheme } from '@/hooks/use-color-scheme'; +import { Colors } from '@/constants/theme' +import { useColorScheme } from '@/hooks/use-color-scheme' export default function TabLayout() { - const colorScheme = useColorScheme(); + const colorScheme = useColorScheme() return ( + }} + > - ); + ) } diff --git a/examples/auth/expo-social-auth/app/+not-found.tsx b/examples/auth/expo-social-auth/app/+not-found.tsx index ca79ddc8ee5..d42cf50c2cf 100644 --- a/examples/auth/expo-social-auth/app/+not-found.tsx +++ b/examples/auth/expo-social-auth/app/+not-found.tsx @@ -1,8 +1,8 @@ -import { Link, Stack } from 'expo-router'; -import { StyleSheet } from 'react-native'; +import { Link, Stack } from 'expo-router' +import { StyleSheet } from 'react-native' -import { ThemedText } from '@/components/themed-text'; -import { ThemedView } from '@/components/themed-view'; +import { ThemedText } from '@/components/themed-text' +import { ThemedView } from '@/components/themed-view' export default function NotFoundScreen() { return ( @@ -15,7 +15,7 @@ export default function NotFoundScreen() { - ); + ) } const styles = StyleSheet.create({ @@ -29,4 +29,4 @@ const styles = StyleSheet.create({ marginTop: 15, paddingVertical: 15, }, -}); +}) diff --git a/examples/auth/expo-social-auth/components/hello-wave.tsx b/examples/auth/expo-social-auth/components/hello-wave.tsx index abfc61edf2e..14fad592dc4 100644 --- a/examples/auth/expo-social-auth/components/hello-wave.tsx +++ b/examples/auth/expo-social-auth/components/hello-wave.tsx @@ -1,34 +1,34 @@ -import { useEffect } from 'react'; -import { StyleSheet } from 'react-native'; +import { useEffect } from 'react' +import { StyleSheet } from 'react-native' import Animated, { useSharedValue, useAnimatedStyle, withTiming, withRepeat, withSequence, -} from 'react-native-reanimated'; +} from 'react-native-reanimated' -import { ThemedText } from '@/components/themed-text'; +import { ThemedText } from '@/components/themed-text' export function HelloWave() { - const rotationAnimation = useSharedValue(0); + const rotationAnimation = useSharedValue(0) useEffect(() => { rotationAnimation.value = withRepeat( withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })), 4 - ); - }, [rotationAnimation]); + ) + }, [rotationAnimation]) const animatedStyle = useAnimatedStyle(() => ({ transform: [{ rotate: `${rotationAnimation.value}deg` }], - })); + })) return ( 👋 - ); + ) } const styles = StyleSheet.create({ @@ -37,4 +37,4 @@ const styles = StyleSheet.create({ lineHeight: 32, marginTop: -6, }, -}); +}) diff --git a/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx b/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx index 93f376484a4..50995ccf349 100644 --- a/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx +++ b/examples/auth/expo-social-auth/components/parallax-scroll-view.tsx @@ -1,30 +1,30 @@ -import type { PropsWithChildren, ReactElement } from 'react'; -import { StyleSheet } from 'react-native'; +import type { PropsWithChildren, ReactElement } from 'react' +import { StyleSheet } from 'react-native' import Animated, { interpolate, useAnimatedRef, useAnimatedStyle, useScrollViewOffset, -} from 'react-native-reanimated'; +} from 'react-native-reanimated' -import { ThemedView } from '@/components/themed-view'; -import { useColorScheme } from '@/hooks/use-color-scheme'; +import { ThemedView } from '@/components/themed-view' +import { useColorScheme } from '@/hooks/use-color-scheme' -const HEADER_HEIGHT = 250; +const HEADER_HEIGHT = 250 type Props = PropsWithChildren<{ - headerImage: ReactElement; - headerBackgroundColor: { dark: string; light: string }; -}>; + headerImage: ReactElement + headerBackgroundColor: { dark: string; light: string } +}> export default function ParallaxScrollView({ children, headerImage, headerBackgroundColor, }: Props) { - const colorScheme = useColorScheme() ?? 'light'; - const scrollRef = useAnimatedRef(); - const scrollOffset = useScrollViewOffset(scrollRef); + const colorScheme = useColorScheme() ?? 'light' + const scrollRef = useAnimatedRef() + const scrollOffset = useScrollViewOffset(scrollRef) const headerAnimatedStyle = useAnimatedStyle(() => { return { @@ -40,8 +40,8 @@ export default function ParallaxScrollView({ scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]), }, ], - }; - }); + } + }) return ( @@ -51,13 +51,14 @@ export default function ParallaxScrollView({ styles.header, { backgroundColor: headerBackgroundColor[colorScheme] }, headerAnimatedStyle, - ]}> + ]} + > {headerImage} {children} - ); + ) } const styles = StyleSheet.create({ @@ -74,4 +75,4 @@ const styles = StyleSheet.create({ gap: 16, overflow: 'hidden', }, -}); +}) diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.android.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.android.tsx index f749d184af5..3a5b2d2d1b6 100644 --- a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.android.tsx +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.android.tsx @@ -1,14 +1,14 @@ -import { supabase } from '@/lib/supabase'; -import { appleAuthAndroid, AppleButton } from '@invertase/react-native-apple-authentication'; -import { SignInWithIdTokenCredentials } from '@supabase/supabase-js'; -import { Platform } from 'react-native'; -import 'react-native-get-random-values'; -import { v4 as uuid } from 'uuid'; +import { supabase } from '@/lib/supabase' +import { appleAuthAndroid, AppleButton } from '@invertase/react-native-apple-authentication' +import { SignInWithIdTokenCredentials } from '@supabase/supabase-js' +import { Platform } from 'react-native' +import 'react-native-get-random-values' +import { v4 as uuid } from 'uuid' async function onAppleButtonPress() { // Generate secure, random values for state and nonce - const rawNonce = uuid(); - const state = uuid(); + const rawNonce = uuid() + const state = uuid() // Configure the request appleAuthAndroid.configure({ @@ -30,11 +30,11 @@ async function onAppleButtonPress() { // Unique state value used to prevent CSRF attacks. A UUID will be generated if nothing is provided. state, - }); + }) // Open the browser window for user sign in - const credentialState = await appleAuthAndroid.signIn(); - console.log('Apple sign in successful:', credentialState); + const credentialState = await appleAuthAndroid.signIn() + console.log('Apple sign in successful:', credentialState) if (credentialState.id_token && credentialState.code && credentialState.nonce) { const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { @@ -42,26 +42,30 @@ async function onAppleButtonPress() { token: credentialState.id_token, nonce: credentialState.nonce, access_token: credentialState.code, - }; + } - const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials); + const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) if (error) { - console.error('Error signing in with Apple:', error); + console.error('Error signing in with Apple:', error) } if (data) { - console.log('Apple sign in successful:', data); + console.log('Apple sign in successful:', data) } } } export default function AppleSignInButton() { - if (Platform.OS !== 'android' || appleAuthAndroid.isSupported !== true) { return <> } + if (Platform.OS !== 'android' || appleAuthAndroid.isSupported !== true) { + return <> + } - return onAppleButtonPress()} - />; + return ( + onAppleButtonPress()} + /> + ) } diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.ios.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.ios.tsx index 599cf00a062..4a4fddcd45f 100644 --- a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.ios.tsx +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.ios.tsx @@ -1,8 +1,8 @@ -import { supabase } from '@/lib/supabase'; -import { AppleButton, appleAuth } from '@invertase/react-native-apple-authentication'; -import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js'; -import { router } from 'expo-router'; -import { Platform } from 'react-native'; +import { supabase } from '@/lib/supabase' +import { AppleButton, appleAuth } from '@invertase/react-native-apple-authentication' +import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js' +import { router } from 'expo-router' +import { Platform } from 'react-native' async function onAppleButtonPress() { // Performs login request @@ -10,42 +10,50 @@ async function onAppleButtonPress() { requestedOperation: appleAuth.Operation.LOGIN, // Note: it appears putting FULL_NAME first is important, see issue #293 requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL], - }); + }) // Get the current authentication state for user // Note: This method must be tested on a real device. On the iOS simulator it always throws an error. - const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user); + const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user) - console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse }); + console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse }) - if (credentialState === appleAuth.State.AUTHORIZED && appleAuthRequestResponse.identityToken && appleAuthRequestResponse.authorizationCode) { + if ( + credentialState === appleAuth.State.AUTHORIZED && + appleAuthRequestResponse.identityToken && + appleAuthRequestResponse.authorizationCode + ) { const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { provider: 'apple', token: appleAuthRequestResponse.identityToken, nonce: appleAuthRequestResponse.nonce, access_token: appleAuthRequestResponse.authorizationCode, - }; + } - const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials); + const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) if (error) { - console.error('Error signing in with Apple:', error); + console.error('Error signing in with Apple:', error) } if (data) { - console.log('Apple sign in successful:', data); - router.navigate('/(tabs)'); + console.log('Apple sign in successful:', data) + router.navigate('/(tabs)') } } } export default function AppleSignInButton() { - if (Platform.OS !== 'ios') { return <>; } + if (Platform.OS !== 'ios') { + return <> + } - return onAppleButtonPress()} - />; + return ( + onAppleButtonPress()} + /> + ) } diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.tsx index 599cf00a062..4a4fddcd45f 100644 --- a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.tsx +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.tsx @@ -1,8 +1,8 @@ -import { supabase } from '@/lib/supabase'; -import { AppleButton, appleAuth } from '@invertase/react-native-apple-authentication'; -import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js'; -import { router } from 'expo-router'; -import { Platform } from 'react-native'; +import { supabase } from '@/lib/supabase' +import { AppleButton, appleAuth } from '@invertase/react-native-apple-authentication' +import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js' +import { router } from 'expo-router' +import { Platform } from 'react-native' async function onAppleButtonPress() { // Performs login request @@ -10,42 +10,50 @@ async function onAppleButtonPress() { requestedOperation: appleAuth.Operation.LOGIN, // Note: it appears putting FULL_NAME first is important, see issue #293 requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL], - }); + }) // Get the current authentication state for user // Note: This method must be tested on a real device. On the iOS simulator it always throws an error. - const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user); + const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user) - console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse }); + console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse }) - if (credentialState === appleAuth.State.AUTHORIZED && appleAuthRequestResponse.identityToken && appleAuthRequestResponse.authorizationCode) { + if ( + credentialState === appleAuth.State.AUTHORIZED && + appleAuthRequestResponse.identityToken && + appleAuthRequestResponse.authorizationCode + ) { const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { provider: 'apple', token: appleAuthRequestResponse.identityToken, nonce: appleAuthRequestResponse.nonce, access_token: appleAuthRequestResponse.authorizationCode, - }; + } - const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials); + const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) if (error) { - console.error('Error signing in with Apple:', error); + console.error('Error signing in with Apple:', error) } if (data) { - console.log('Apple sign in successful:', data); - router.navigate('/(tabs)'); + console.log('Apple sign in successful:', data) + router.navigate('/(tabs)') } } } export default function AppleSignInButton() { - if (Platform.OS !== 'ios') { return <>; } + if (Platform.OS !== 'ios') { + return <> + } - return onAppleButtonPress()} - />; + return ( + onAppleButtonPress()} + /> + ) } diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.web.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.web.tsx index fa1befa6121..64e0e756e21 100644 --- a/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.web.tsx +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/apple/apple-sign-in-button.web.tsx @@ -1,68 +1,77 @@ -import { supabase } from '@/lib/supabase'; -import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js'; -import { useEffect, useState } from 'react'; -import AppleSignin, { type AppleAuthResponse } from 'react-apple-signin-auth'; -import { Platform } from 'react-native'; +import { supabase } from '@/lib/supabase' +import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js' +import { useEffect, useState } from 'react' +import AppleSignin, { type AppleAuthResponse } from 'react-apple-signin-auth' +import { Platform } from 'react-native' export default function AppleSignInButton() { - const [nonce, setNonce] = useState(''); - const [sha256Nonce, setSha256Nonce] = useState(''); + const [nonce, setNonce] = useState('') + const [sha256Nonce, setSha256Nonce] = useState('') async function onAppleButtonSuccess(appleAuthRequestResponse: AppleAuthResponse) { - console.debug('Apple sign in successful:', { appleAuthRequestResponse }); - if (appleAuthRequestResponse.authorization && appleAuthRequestResponse.authorization.id_token && appleAuthRequestResponse.authorization.code) { + console.debug('Apple sign in successful:', { appleAuthRequestResponse }) + if ( + appleAuthRequestResponse.authorization && + appleAuthRequestResponse.authorization.id_token && + appleAuthRequestResponse.authorization.code + ) { const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { provider: 'apple', token: appleAuthRequestResponse.authorization.id_token, nonce, access_token: appleAuthRequestResponse.authorization.code, - }; + } const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) if (error) { - console.error('Error signing in with Apple:', error); + console.error('Error signing in with Apple:', error) } if (data) { - console.log('Apple sign in successful:', data); + console.log('Apple sign in successful:', data) } - }; + } } useEffect(() => { function generateNonce(): string { - const array = new Uint32Array(1); - window.crypto.getRandomValues(array); - return array[0].toString(); - }; - - async function generateSha256Nonce(nonce: string): Promise { - const buffer = await window.crypto.subtle.digest('sha-256', new TextEncoder().encode(nonce)); - const array = Array.from(new Uint8Array(buffer)); - return array.map(b => b.toString(16).padStart(2, '0')).join(''); + const array = new Uint32Array(1) + window.crypto.getRandomValues(array) + return array[0].toString() } - let nonce = generateNonce(); - setNonce(nonce); + async function generateSha256Nonce(nonce: string): Promise { + const buffer = await window.crypto.subtle.digest('sha-256', new TextEncoder().encode(nonce)) + const array = Array.from(new Uint8Array(buffer)) + return array.map((b) => b.toString(16).padStart(2, '0')).join('') + } - generateSha256Nonce(nonce) - .then((sha256Nonce) => { setSha256Nonce(sha256Nonce) }); - }, []); + let nonce = generateNonce() + setNonce(nonce) - if (Platform.OS !== 'web') { return <>; } + generateSha256Nonce(nonce).then((sha256Nonce) => { + setSha256Nonce(sha256Nonce) + }) + }, []) - return console.error('Apple sign in error:', error)} - />; + if (Platform.OS !== 'web') { + return <> + } + + return ( + console.error('Apple sign in error:', error)} + /> + ) } diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.tsx index 6962d83c38e..2ffb5b4f064 100644 --- a/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.tsx +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.tsx @@ -1,90 +1,89 @@ -import { supabase } from '@/lib/supabase'; -import { useEffect } from 'react'; -import { TouchableOpacity } from 'react-native'; +import { supabase } from '@/lib/supabase' +import { useEffect } from 'react' +import { TouchableOpacity } from 'react-native' -import { expo } from '@/app.json'; -import { Text } from '@react-navigation/elements'; -import { Image } from 'expo-image'; -import * as WebBrowser from "expo-web-browser"; +import { expo } from '@/app.json' +import { Text } from '@react-navigation/elements' +import { Image } from 'expo-image' +import * as WebBrowser from 'expo-web-browser' -WebBrowser.maybeCompleteAuthSession(); +WebBrowser.maybeCompleteAuthSession() export default function GoogleSignInButton() { - function extractParamsFromUrl(url: string) { - const parsedUrl = new URL(url); - const hash = parsedUrl.hash.substring(1); // Remove the leading '#' - const params = new URLSearchParams(hash); + const parsedUrl = new URL(url) + const hash = parsedUrl.hash.substring(1) // Remove the leading '#' + const params = new URLSearchParams(hash) return { - access_token: params.get("access_token"), - expires_in: parseInt(params.get("expires_in") || "0"), - refresh_token: params.get("refresh_token"), - token_type: params.get("token_type"), - provider_token: params.get("provider_token"), - code: params.get("code"), - }; - }; + access_token: params.get('access_token'), + expires_in: parseInt(params.get('expires_in') || '0'), + refresh_token: params.get('refresh_token'), + token_type: params.get('token_type'), + provider_token: params.get('provider_token'), + code: params.get('code'), + } + } async function onSignInButtonPress() { - console.debug('onSignInButtonPress - start'); + console.debug('onSignInButtonPress - start') const res = await supabase.auth.signInWithOAuth({ - provider: "google", + provider: 'google', options: { redirectTo: `${expo.scheme}://google-auth`, - queryParams: { prompt: "consent" }, + queryParams: { prompt: 'consent' }, skipBrowserRedirect: true, }, - }); + }) - const googleOAuthUrl = res.data.url; + const googleOAuthUrl = res.data.url if (!googleOAuthUrl) { - console.error("no oauth url found!"); - return; + console.error('no oauth url found!') + return } const result = await WebBrowser.openAuthSessionAsync( googleOAuthUrl, `${expo.scheme}://google-auth`, - { showInRecents: true }, + { showInRecents: true } ).catch((err) => { - console.error('onSignInButtonPress - openAuthSessionAsync - error', { err }); - console.log(err); - }); + console.error('onSignInButtonPress - openAuthSessionAsync - error', { err }) + console.log(err) + }) - console.debug('onSignInButtonPress - openAuthSessionAsync - result', { result }); + console.debug('onSignInButtonPress - openAuthSessionAsync - result', { result }) - if (result && result.type === "success") { - console.debug('onSignInButtonPress - openAuthSessionAsync - success'); - const params = extractParamsFromUrl(result.url); - console.debug('onSignInButtonPress - openAuthSessionAsync - success', { params }); + if (result && result.type === 'success') { + console.debug('onSignInButtonPress - openAuthSessionAsync - success') + const params = extractParamsFromUrl(result.url) + console.debug('onSignInButtonPress - openAuthSessionAsync - success', { params }) if (params.access_token && params.refresh_token) { - console.debug('onSignInButtonPress - setSession'); + console.debug('onSignInButtonPress - setSession') const { data, error } = await supabase.auth.setSession({ access_token: params.access_token, refresh_token: params.refresh_token, - }); - console.debug('onSignInButtonPress - setSession - success', { data, error }); - return; + }) + console.debug('onSignInButtonPress - setSession - success', { data, error }) + return } else { - console.error('onSignInButtonPress - setSession - failed'); + console.error('onSignInButtonPress - setSession - failed') // sign in/up failed } } else { - console.error('onSignInButtonPress - openAuthSessionAsync - failed'); + console.error('onSignInButtonPress - openAuthSessionAsync - failed') } } // to warm up the browser useEffect(() => { - WebBrowser.warmUpAsync(); + WebBrowser.warmUpAsync() return () => { - WebBrowser.coolDownAsync(); - }; - }, []); + WebBrowser.coolDownAsync() + } + }, []) return ( - ); + ) } diff --git a/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.web.tsx b/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.web.tsx index 36d60d9be2c..1e446d9b927 100644 --- a/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.web.tsx +++ b/examples/auth/expo-social-auth/components/social-auth-buttons/google/google-sign-in-button.web.tsx @@ -1,60 +1,60 @@ -import { supabase } from '@/lib/supabase'; -import { CredentialResponse, GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google'; -import { SignInWithIdTokenCredentials } from '@supabase/supabase-js'; -import { useEffect, useState } from 'react'; +import { supabase } from '@/lib/supabase' +import { CredentialResponse, GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google' +import { SignInWithIdTokenCredentials } from '@supabase/supabase-js' +import { useEffect, useState } from 'react' -import 'react-native-get-random-values'; +import 'react-native-get-random-values' export default function GoogleSignInButton() { - // Generate secure, random values for state and nonce - const [nonce, setNonce] = useState(''); - const [sha256Nonce, setSha256Nonce] = useState(''); + const [nonce, setNonce] = useState('') + const [sha256Nonce, setSha256Nonce] = useState('') async function onGoogleButtonSuccess(authRequestResponse: CredentialResponse) { - console.debug('Google sign in successful:', { authRequestResponse }); + console.debug('Google sign in successful:', { authRequestResponse }) if (authRequestResponse.clientId && authRequestResponse.credential) { const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { provider: 'google', token: authRequestResponse.credential, nonce: nonce, - }; + } - const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials); + const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) if (error) { - console.error('Error signing in with Google:', error); + console.error('Error signing in with Google:', error) } if (data) { - console.log('Google sign in successful:', data); + console.log('Google sign in successful:', data) } } } function onGoogleButtonFailure() { - console.error('Error signing in with Google'); + console.error('Error signing in with Google') } useEffect(() => { function generateNonce(): string { - const array = new Uint32Array(1); - window.crypto.getRandomValues(array); - return array[0].toString(); + const array = new Uint32Array(1) + window.crypto.getRandomValues(array) + return array[0].toString() } async function generateSha256Nonce(nonce: string): Promise { - const buffer = await window.crypto.subtle.digest('sha-256', new TextEncoder().encode(nonce)); - const array = Array.from(new Uint8Array(buffer)); - return array.map(b => b.toString(16).padStart(2, '0')).join(''); + const buffer = await window.crypto.subtle.digest('sha-256', new TextEncoder().encode(nonce)) + const array = Array.from(new Uint8Array(buffer)) + return array.map((b) => b.toString(16).padStart(2, '0')).join('') } - let nonce = generateNonce(); - setNonce(nonce); + let nonce = generateNonce() + setNonce(nonce) - generateSha256Nonce(nonce) - .then((sha256Nonce) => { setSha256Nonce(sha256Nonce) }); - }, []); + generateSha256Nonce(nonce).then((sha256Nonce) => { + setSha256Nonce(sha256Nonce) + }) + }, []) return ( - ); + ) } diff --git a/examples/auth/expo-social-auth/components/themed-text.tsx b/examples/auth/expo-social-auth/components/themed-text.tsx index f16b689b1b7..a6e900f0623 100644 --- a/examples/auth/expo-social-auth/components/themed-text.tsx +++ b/examples/auth/expo-social-auth/components/themed-text.tsx @@ -1,13 +1,13 @@ -import { Text, type TextProps, StyleSheet } from 'react-native'; +import { Text, type TextProps, StyleSheet } from 'react-native' -import { useThemeColor } from '@/hooks/use-theme-color'; -import { Fonts } from '@/constants/theme'; +import { useThemeColor } from '@/hooks/use-theme-color' +import { Fonts } from '@/constants/theme' export type ThemedTextProps = TextProps & { - lightColor?: string; - darkColor?: string; - type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; -}; + lightColor?: string + darkColor?: string + type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' +} export function ThemedText({ style, @@ -16,7 +16,7 @@ export function ThemedText({ type = 'default', ...rest }: ThemedTextProps) { - const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); + const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text') return ( - ); + ) } const styles = StyleSheet.create({ @@ -58,4 +58,4 @@ const styles = StyleSheet.create({ fontSize: 16, color: '#0a7ea4', }, -}); +}) diff --git a/examples/auth/expo-social-auth/components/themed-view.tsx b/examples/auth/expo-social-auth/components/themed-view.tsx index 6f181d826ea..fe24772c1c0 100644 --- a/examples/auth/expo-social-auth/components/themed-view.tsx +++ b/examples/auth/expo-social-auth/components/themed-view.tsx @@ -1,14 +1,14 @@ -import { View, type ViewProps } from 'react-native'; +import { View, type ViewProps } from 'react-native' -import { useThemeColor } from '@/hooks/use-theme-color'; +import { useThemeColor } from '@/hooks/use-theme-color' export type ThemedViewProps = ViewProps & { - lightColor?: string; - darkColor?: string; -}; + lightColor?: string + darkColor?: string +} export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { - const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); + const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background') - return ; + return } diff --git a/examples/auth/expo-social-auth/constants/theme.ts b/examples/auth/expo-social-auth/constants/theme.ts index f06facd2f6e..97686da8b7a 100644 --- a/examples/auth/expo-social-auth/constants/theme.ts +++ b/examples/auth/expo-social-auth/constants/theme.ts @@ -3,10 +3,10 @@ * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. */ -import { Platform } from 'react-native'; +import { Platform } from 'react-native' -const tintColorLight = '#0a7ea4'; -const tintColorDark = '#fff'; +const tintColorLight = '#0a7ea4' +const tintColorDark = '#fff' export const Colors = { light: { @@ -25,7 +25,7 @@ export const Colors = { tabIconDefault: '#9BA1A6', tabIconSelected: tintColorDark, }, -}; +} export const Fonts = Platform.select({ ios: { @@ -50,4 +50,4 @@ export const Fonts = Platform.select({ rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", }, -}); +}) diff --git a/examples/auth/expo-social-auth/eslint.config.js b/examples/auth/expo-social-auth/eslint.config.js index 5025da683d6..ea589baa12f 100644 --- a/examples/auth/expo-social-auth/eslint.config.js +++ b/examples/auth/expo-social-auth/eslint.config.js @@ -1,10 +1,10 @@ // https://docs.expo.dev/guides/using-eslint/ -const { defineConfig } = require('eslint/config'); -const expoConfig = require('eslint-config-expo/flat'); +const { defineConfig } = require('eslint/config') +const expoConfig = require('eslint-config-expo/flat') module.exports = defineConfig([ expoConfig, { ignores: ['dist/*'], }, -]); +]) diff --git a/examples/auth/expo-social-auth/hooks/use-color-scheme.ts b/examples/auth/expo-social-auth/hooks/use-color-scheme.ts index 17e3c63e1ac..b370337ae82 100644 --- a/examples/auth/expo-social-auth/hooks/use-color-scheme.ts +++ b/examples/auth/expo-social-auth/hooks/use-color-scheme.ts @@ -1 +1 @@ -export { useColorScheme } from 'react-native'; +export { useColorScheme } from 'react-native' diff --git a/examples/auth/expo-social-auth/hooks/use-color-scheme.web.ts b/examples/auth/expo-social-auth/hooks/use-color-scheme.web.ts index 7eb1c1b7071..2a690de8e92 100644 --- a/examples/auth/expo-social-auth/hooks/use-color-scheme.web.ts +++ b/examples/auth/expo-social-auth/hooks/use-color-scheme.web.ts @@ -1,21 +1,21 @@ -import { useEffect, useState } from 'react'; -import { useColorScheme as useRNColorScheme } from 'react-native'; +import { useEffect, useState } from 'react' +import { useColorScheme as useRNColorScheme } from 'react-native' /** * To support static rendering, this value needs to be re-calculated on the client side for web */ export function useColorScheme() { - const [hasHydrated, setHasHydrated] = useState(false); + const [hasHydrated, setHasHydrated] = useState(false) useEffect(() => { - setHasHydrated(true); - }, []); + setHasHydrated(true) + }, []) - const colorScheme = useRNColorScheme(); + const colorScheme = useRNColorScheme() if (hasHydrated) { - return colorScheme; + return colorScheme } - return 'light'; + return 'light' } diff --git a/examples/auth/expo-social-auth/hooks/use-theme-color.ts b/examples/auth/expo-social-auth/hooks/use-theme-color.ts index 0cbc3a6e6f9..d0137b2a5ea 100644 --- a/examples/auth/expo-social-auth/hooks/use-theme-color.ts +++ b/examples/auth/expo-social-auth/hooks/use-theme-color.ts @@ -3,19 +3,19 @@ * https://docs.expo.dev/guides/color-schemes/ */ -import { Colors } from '@/constants/theme'; -import { useColorScheme } from '@/hooks/use-color-scheme'; +import { Colors } from '@/constants/theme' +import { useColorScheme } from '@/hooks/use-color-scheme' export function useThemeColor( props: { light?: string; dark?: string }, colorName: keyof typeof Colors.light & keyof typeof Colors.dark ) { - const theme = useColorScheme() ?? 'light'; - const colorFromProps = props[theme]; + const theme = useColorScheme() ?? 'light' + const colorFromProps = props[theme] if (colorFromProps) { - return colorFromProps; + return colorFromProps } else { - return Colors[theme][colorName]; + return Colors[theme][colorName] } } diff --git a/examples/auth/expo-social-auth/lib/supabase.ts b/examples/auth/expo-social-auth/lib/supabase.ts index 0f2096b9871..a207641a2dd 100644 --- a/examples/auth/expo-social-auth/lib/supabase.ts +++ b/examples/auth/expo-social-auth/lib/supabase.ts @@ -1,21 +1,23 @@ -import { createClient } from '@supabase/supabase-js'; -import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store'; +import { createClient } from '@supabase/supabase-js' +import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store' const ExpoSecureStoreAdapter = { getItem: (key: string) => { - console.debug("getItem", { key, getItemAsync }) + console.debug('getItem', { key, getItemAsync }) return getItemAsync(key) }, setItem: (key: string, value: string) => { if (value.length > 2048) { - console.warn('Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.') + console.warn( + 'Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.' + ) } return setItemAsync(key, value) }, removeItem: (key: string) => { return deleteItemAsync(key) }, -}; +} export const supabase = createClient( process.env.EXPO_PUBLIC_SUPABASE_URL ?? '', @@ -27,5 +29,5 @@ export const supabase = createClient( persistSession: true, detectSessionInUrl: false, }, - }, -); + } +) diff --git a/examples/auth/expo-social-auth/lib/supabase.web.ts b/examples/auth/expo-social-auth/lib/supabase.web.ts index 882c2b4b21d..e7054507ed1 100644 --- a/examples/auth/expo-social-auth/lib/supabase.web.ts +++ b/examples/auth/expo-social-auth/lib/supabase.web.ts @@ -1,24 +1,24 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { createClient } from '@supabase/supabase-js'; -import 'react-native-url-polyfill/auto'; +import AsyncStorage from '@react-native-async-storage/async-storage' +import { createClient } from '@supabase/supabase-js' +import 'react-native-url-polyfill/auto' -const isSSR = typeof window === 'undefined'; +const isSSR = typeof window === 'undefined' const ExpoWebSecureStoreAdapter = { getItem: (key: string) => { - if (isSSR) return null; - console.debug("getItem", { key }) + if (isSSR) return null + console.debug('getItem', { key }) return AsyncStorage.getItem(key) }, setItem: (key: string, value: string) => { - if (isSSR) return; + if (isSSR) return return AsyncStorage.setItem(key, value) }, removeItem: (key: string) => { - if (isSSR) return; + if (isSSR) return return AsyncStorage.removeItem(key) }, -}; +} export const supabase = createClient( process.env.EXPO_PUBLIC_SUPABASE_URL ?? '', @@ -30,5 +30,5 @@ export const supabase = createClient( persistSession: true, detectSessionInUrl: false, }, - }, -); + } +) diff --git a/examples/auth/expo-social-auth/package-lock.json b/examples/auth/expo-social-auth/package-lock.json index 05c0dfd77e4..958a5a7d6f9 100644 --- a/examples/auth/expo-social-auth/package-lock.json +++ b/examples/auth/expo-social-auth/package-lock.json @@ -4231,219 +4231,157 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] + "os": ["android"] }, "node_modules/@unrs/resolver-binding-android-arm64": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] + "os": ["android"] }, "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ] + "os": ["darwin"] }, "node_modules/@unrs/resolver-binding-darwin-x64": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ] + "os": ["darwin"] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ] + "os": ["freebsd"] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], + "cpu": ["ppc64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], + "cpu": ["riscv64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], + "cpu": ["riscv64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], + "cpu": ["s390x"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], + "cpu": ["wasm32"], "dev": true, "license": "MIT", "optional": true, @@ -4458,43 +4396,31 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "os": ["win32"] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "os": ["win32"] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "os": ["win32"] }, "node_modules/@urql/core": { "version": "5.2.0", @@ -7304,9 +7230,7 @@ "hasInstallScript": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -8811,14 +8735,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "license": "MPL-2.0", "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">= 12.0.0" }, @@ -8831,14 +8751,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "license": "MPL-2.0", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">= 12.0.0" }, @@ -8851,14 +8767,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "license": "MPL-2.0", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">= 12.0.0" }, @@ -8871,14 +8783,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "license": "MPL-2.0", "optional": true, - "os": [ - "freebsd" - ], + "os": ["freebsd"], "engines": { "node": ">= 12.0.0" }, @@ -8891,14 +8799,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "license": "MPL-2.0", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">= 12.0.0" }, @@ -8911,14 +8815,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "license": "MPL-2.0", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">= 12.0.0" }, @@ -8931,14 +8831,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "license": "MPL-2.0", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">= 12.0.0" }, @@ -8951,14 +8847,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "license": "MPL-2.0", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">= 12.0.0" }, @@ -8971,14 +8863,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "license": "MPL-2.0", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">= 12.0.0" }, @@ -8991,14 +8879,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "license": "MPL-2.0", "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">= 12.0.0" }, @@ -9011,14 +8895,10 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "license": "MPL-2.0", "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">= 12.0.0" }, @@ -12897,10 +12777,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "funding": ["https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan"], "license": "MIT", "bin": { "uuid": "dist-node/bin/uuid" diff --git a/examples/auth/expo-social-auth/providers/auth-provider.tsx b/examples/auth/expo-social-auth/providers/auth-provider.tsx index f351dd2f595..760424dae96 100644 --- a/examples/auth/expo-social-auth/providers/auth-provider.tsx +++ b/examples/auth/expo-social-auth/providers/auth-provider.tsx @@ -44,11 +44,7 @@ export default function AuthProvider({ children }: PropsWithChildren) { setIsLoading(true) if (claims) { - const { data } = await supabase - .from('profiles') - .select('*') - .eq('id', claims.sub) - .single() + const { data } = await supabase.from('profiles').select('*').eq('id', claims.sub).single() setProfile(data) } else { diff --git a/examples/auth/expo-social-auth/scripts/reset-project.js b/examples/auth/expo-social-auth/scripts/reset-project.js index 51dff15acba..6f6763a21e1 100755 --- a/examples/auth/expo-social-auth/scripts/reset-project.js +++ b/examples/auth/expo-social-auth/scripts/reset-project.js @@ -6,15 +6,15 @@ * You can remove the `reset-project` script from package.json and safely delete this file after running it. */ -const fs = require("fs"); -const path = require("path"); -const readline = require("readline"); +const fs = require('fs') +const path = require('path') +const readline = require('readline') -const root = process.cwd(); -const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; -const exampleDir = "app-example"; -const newAppDir = "app"; -const exampleDirPath = path.join(root, exampleDir); +const root = process.cwd() +const oldDirs = ['app', 'components', 'hooks', 'constants', 'scripts'] +const exampleDir = 'app-example' +const newAppDir = 'app' +const exampleDirPath = path.join(root, exampleDir) const indexContent = `import { Text, View } from "react-native"; @@ -31,82 +31,82 @@ export default function Index() { ); } -`; +` const layoutContent = `import { Stack } from "expo-router"; export default function RootLayout() { return ; } -`; +` const rl = readline.createInterface({ input: process.stdin, output: process.stdout, -}); +}) const moveDirectories = async (userInput) => { try { - if (userInput === "y") { + if (userInput === 'y') { // Create the app-example directory - await fs.promises.mkdir(exampleDirPath, { recursive: true }); - console.log(`📁 /${exampleDir} directory created.`); + await fs.promises.mkdir(exampleDirPath, { recursive: true }) + console.log(`📁 /${exampleDir} directory created.`) } // Move old directories to new app-example directory or delete them for (const dir of oldDirs) { - const oldDirPath = path.join(root, dir); + const oldDirPath = path.join(root, dir) if (fs.existsSync(oldDirPath)) { - if (userInput === "y") { - const newDirPath = path.join(root, exampleDir, dir); - await fs.promises.rename(oldDirPath, newDirPath); - console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`); + if (userInput === 'y') { + const newDirPath = path.join(root, exampleDir, dir) + await fs.promises.rename(oldDirPath, newDirPath) + console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`) } else { - await fs.promises.rm(oldDirPath, { recursive: true, force: true }); - console.log(`❌ /${dir} deleted.`); + await fs.promises.rm(oldDirPath, { recursive: true, force: true }) + console.log(`❌ /${dir} deleted.`) } } else { - console.log(`➡️ /${dir} does not exist, skipping.`); + console.log(`➡️ /${dir} does not exist, skipping.`) } } // Create new /app directory - const newAppDirPath = path.join(root, newAppDir); - await fs.promises.mkdir(newAppDirPath, { recursive: true }); - console.log("\n📁 New /app directory created."); + const newAppDirPath = path.join(root, newAppDir) + await fs.promises.mkdir(newAppDirPath, { recursive: true }) + console.log('\n📁 New /app directory created.') // Create index.tsx - const indexPath = path.join(newAppDirPath, "index.tsx"); - await fs.promises.writeFile(indexPath, indexContent); - console.log("📄 app/index.tsx created."); + const indexPath = path.join(newAppDirPath, 'index.tsx') + await fs.promises.writeFile(indexPath, indexContent) + console.log('📄 app/index.tsx created.') // Create _layout.tsx - const layoutPath = path.join(newAppDirPath, "_layout.tsx"); - await fs.promises.writeFile(layoutPath, layoutContent); - console.log("📄 app/_layout.tsx created."); + const layoutPath = path.join(newAppDirPath, '_layout.tsx') + await fs.promises.writeFile(layoutPath, layoutContent) + console.log('📄 app/_layout.tsx created.') - console.log("\n✅ Project reset complete. Next steps:"); + console.log('\n✅ Project reset complete. Next steps:') console.log( `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ - userInput === "y" + userInput === 'y' ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` - : "" + : '' }` - ); + ) } catch (error) { - console.error(`❌ Error during script execution: ${error.message}`); + console.error(`❌ Error during script execution: ${error.message}`) } -}; +} rl.question( - "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", + 'Do you want to move existing files to /app-example instead of deleting them? (Y/n): ', (answer) => { - const userInput = answer.trim().toLowerCase() || "y"; - if (userInput === "y" || userInput === "n") { - moveDirectories(userInput).finally(() => rl.close()); + const userInput = answer.trim().toLowerCase() || 'y' + if (userInput === 'y' || userInput === 'n') { + moveDirectories(userInput).finally(() => rl.close()) } else { - console.log("❌ Invalid input. Please enter 'Y' or 'N'."); - rl.close(); + console.log("❌ Invalid input. Please enter 'Y' or 'N'.") + rl.close() } } -); +) diff --git a/examples/auth/expo-social-auth/tsconfig.json b/examples/auth/expo-social-auth/tsconfig.json index 909e9010867..ce27fee398f 100644 --- a/examples/auth/expo-social-auth/tsconfig.json +++ b/examples/auth/expo-social-auth/tsconfig.json @@ -3,15 +3,8 @@ "compilerOptions": { "strict": true, "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, - "include": [ - "**/*.ts", - "**/*.tsx", - ".expo/types/**/*.ts", - "expo-env.d.ts" - ] + "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] } diff --git a/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fab2d9..e882ab98894 100644 --- a/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,122 @@ { - "images" : [ + "images": [ { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "size": "20x20", + "idiom": "iphone", + "filename": "Icon-App-20x20@2x.png", + "scale": "2x" }, { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" + "size": "20x20", + "idiom": "iphone", + "filename": "Icon-App-20x20@3x.png", + "scale": "3x" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@1x.png", + "scale": "1x" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@2x.png", + "scale": "2x" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@3x.png", + "scale": "3x" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "size": "40x40", + "idiom": "iphone", + "filename": "Icon-App-40x40@2x.png", + "scale": "2x" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "size": "40x40", + "idiom": "iphone", + "filename": "Icon-App-40x40@3x.png", + "scale": "3x" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@2x.png", + "scale": "2x" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@3x.png", + "scale": "3x" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@1x.png", + "scale": "1x" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@2x.png", + "scale": "2x" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@1x.png", + "scale": "1x" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@2x.png", + "scale": "2x" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@1x.png", + "scale": "1x" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@2x.png", + "scale": "2x" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "size": "76x76", + "idiom": "ipad", + "filename": "Icon-App-76x76@1x.png", + "scale": "1x" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "size": "76x76", + "idiom": "ipad", + "filename": "Icon-App-76x76@2x.png", + "scale": "2x" }, { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "size": "83.5x83.5", + "idiom": "ipad", + "filename": "Icon-App-83.5x83.5@2x.png", + "scale": "2x" }, { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" + "size": "1024x1024", + "idiom": "ios-marketing", + "filename": "Icon-App-1024x1024@1x.png", + "scale": "1x" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "version": 1, + "author": "xcode" } } diff --git a/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json index 0bedcf2fd46..781d7cdcac0 100644 --- a/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ b/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -1,23 +1,23 @@ { - "images" : [ + "images": [ { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" + "idiom": "universal", + "filename": "LaunchImage.png", + "scale": "1x" }, { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" + "idiom": "universal", + "filename": "LaunchImage@2x.png", + "scale": "2x" }, { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" + "idiom": "universal", + "filename": "LaunchImage@3x.png", + "scale": "3x" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "version": 1, + "author": "xcode" } } diff --git a/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md index 89c2725b70f..b5b843ad3b8 100644 --- a/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ b/examples/auth/flutter-mfa/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -2,4 +2,4 @@ You can customize the launch screen with your own desired assets by replacing the image files in this directory. -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. diff --git a/examples/auth/flutter-mfa/web/manifest.json b/examples/auth/flutter-mfa/web/manifest.json index 222e13d6e91..03c5c55da8c 100644 --- a/examples/auth/flutter-mfa/web/manifest.json +++ b/examples/auth/flutter-mfa/web/manifest.json @@ -1,35 +1,35 @@ { - "name": "mfa_app", - "short_name": "mfa_app", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] + "name": "mfa_app", + "short_name": "mfa_app", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] } diff --git a/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fab2d9..e882ab98894 100644 --- a/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,122 @@ { - "images" : [ + "images": [ { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "size": "20x20", + "idiom": "iphone", + "filename": "Icon-App-20x20@2x.png", + "scale": "2x" }, { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" + "size": "20x20", + "idiom": "iphone", + "filename": "Icon-App-20x20@3x.png", + "scale": "3x" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@1x.png", + "scale": "1x" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@2x.png", + "scale": "2x" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@3x.png", + "scale": "3x" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "size": "40x40", + "idiom": "iphone", + "filename": "Icon-App-40x40@2x.png", + "scale": "2x" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "size": "40x40", + "idiom": "iphone", + "filename": "Icon-App-40x40@3x.png", + "scale": "3x" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@2x.png", + "scale": "2x" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@3x.png", + "scale": "3x" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@1x.png", + "scale": "1x" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@2x.png", + "scale": "2x" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@1x.png", + "scale": "1x" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@2x.png", + "scale": "2x" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@1x.png", + "scale": "1x" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@2x.png", + "scale": "2x" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "size": "76x76", + "idiom": "ipad", + "filename": "Icon-App-76x76@1x.png", + "scale": "1x" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "size": "76x76", + "idiom": "ipad", + "filename": "Icon-App-76x76@2x.png", + "scale": "2x" }, { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "size": "83.5x83.5", + "idiom": "ipad", + "filename": "Icon-App-83.5x83.5@2x.png", + "scale": "2x" }, { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" + "size": "1024x1024", + "idiom": "ios-marketing", + "filename": "Icon-App-1024x1024@1x.png", + "scale": "1x" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "version": 1, + "author": "xcode" } } diff --git a/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json index 0bedcf2fd46..781d7cdcac0 100644 --- a/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ b/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -1,23 +1,23 @@ { - "images" : [ + "images": [ { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" + "idiom": "universal", + "filename": "LaunchImage.png", + "scale": "1x" }, { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" + "idiom": "universal", + "filename": "LaunchImage@2x.png", + "scale": "2x" }, { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" + "idiom": "universal", + "filename": "LaunchImage@3x.png", + "scale": "3x" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "version": 1, + "author": "xcode" } } diff --git a/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md index 89c2725b70f..b5b843ad3b8 100644 --- a/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ b/examples/auth/flutter-native-google-auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -2,4 +2,4 @@ You can customize the launch screen with your own desired assets by replacing the image files in this directory. -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. diff --git a/examples/auth/hono-full/package-lock.json b/examples/auth/hono-full/package-lock.json index 0845eac4371..70d88021ffc 100644 --- a/examples/auth/hono-full/package-lock.json +++ b/examples/auth/hono-full/package-lock.json @@ -22,15 +22,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], + "cpu": ["ppc64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], + "os": ["aix"], "engines": { "node": ">=12" } @@ -39,15 +35,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -56,15 +48,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -73,15 +61,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -90,15 +74,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">=12" } @@ -107,15 +87,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">=12" } @@ -124,15 +100,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ], + "os": ["freebsd"], "engines": { "node": ">=12" } @@ -141,15 +113,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ], + "os": ["freebsd"], "engines": { "node": ">=12" } @@ -158,15 +126,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -175,15 +139,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -192,15 +152,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -209,15 +165,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], + "cpu": ["loong64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -226,15 +178,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "cpu": ["mips64el"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -243,15 +191,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "cpu": ["ppc64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -260,15 +204,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "cpu": ["riscv64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -277,15 +217,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "cpu": ["s390x"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -294,15 +230,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -311,15 +243,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "netbsd" - ], + "os": ["netbsd"], "engines": { "node": ">=12" } @@ -328,15 +256,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "openbsd" - ], + "os": ["openbsd"], "engines": { "node": ">=12" } @@ -345,15 +269,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "sunos" - ], + "os": ["sunos"], "engines": { "node": ">=12" } @@ -362,15 +282,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -379,15 +295,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -396,15 +308,11 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -468,267 +376,191 @@ "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.0.tgz", "integrity": "sha512-qFcFto9figFLz2g25DxJ1WWL9+c91fTxnGuwhToCl8BaqDsDYMl/kOnBXAyAqkkzAWimYMSWNPWEjt+ADAHuoQ==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] + "os": ["android"] }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.0.tgz", "integrity": "sha512-vqrQdusvVl7dthqNjWCL043qelBK+gv9v3ZiqdxgaJvmZyIAAXMjeGVSqZynKq69T7062T5VrVTuikKSAAVP6A==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] + "os": ["android"] }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.0.tgz", "integrity": "sha512-617pd92LhdA9+wpixnzsyhVft3szYiN16aNUMzVkf2N+yAk8UXY226Bfp36LvxYTUt7MO/ycqGFjQgJ0wlMaWQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ] + "os": ["darwin"] }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.0.tgz", "integrity": "sha512-Y3b4oDoaEhCypg8ajPqigKDcpi5ZZovemQl9Edpem0uNv6UUjXv7iySBpGIUTSs2ovWOzYpfw9EbFJXF/fJHWw==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ] + "os": ["darwin"] }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.0.tgz", "integrity": "sha512-3REQJ4f90sFIBfa0BUokiCdrV/E4uIjhkWe1bMgCkhFXbf4D8YN6C4zwJL881GM818qVYE9BO3dGwjKhpo2ABA==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ] + "os": ["freebsd"] }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.0.tgz", "integrity": "sha512-ZtY3Y8icbe3Cc+uQicsXG5L+CRGUfLZjW6j2gn5ikpltt3Whqjfo5mkyZ86UiuHF9Q3ZsaQeW7YswlHnN+lAcg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ] + "os": ["freebsd"] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.0.tgz", "integrity": "sha512-bsPGGzfiHXMhQGuFGpmo2PyTwcrh2otL6ycSZAFTESviUoBOuxF7iBbAL5IJXc/69peXl5rAtbewBFeASZ9O0g==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.0.tgz", "integrity": "sha512-kvyIECEhs2DrrdfQf++maCWJIQ974EI4txlz1nNSBaCdtf7i5Xf1AQCEJWOC5rEBisdaMFFnOWNLYt7KpFqy5A==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.0.tgz", "integrity": "sha512-CFE7zDNrokaotXu+shwIrmWrFxllg79vciH4E/zeK7NitVuWEaXRzS0mFfFvyhZfn8WfVOG/1E9u8/DFEgK7WQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.0.tgz", "integrity": "sha512-MctNTBlvMcIBP0t8lV/NXiUwFg9oK5F79CxLU+a3xgrdJjfBLVIEHSAjQ9+ipofN2GKaMLnFFXLltg1HEEPaGQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.0.tgz", "integrity": "sha512-fBpoYwLEPivL3q368+gwn4qnYnr7GVwM6NnMo8rJ4wb0p/Y5lg88vQRRP077gf+tc25akuqd+1Sxbn9meODhwA==", - "cpu": [ - "loong64" - ], + "cpu": ["loong64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.0.tgz", "integrity": "sha512-1hiHPV6dUaqIMXrIjN+vgJqtfkLpqHS1Xsg0oUfUVD98xGp1wX89PIXgDF2DWra1nxAd8dfE0Dk59MyeKaBVAw==", - "cpu": [ - "ppc64" - ], + "cpu": ["ppc64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.0.tgz", "integrity": "sha512-U0xcC80SMpEbvvLw92emHrNjlS3OXjAM0aVzlWfar6PR0ODWCTQtKeeB+tlAPGfZQXicv1SpWwRz9Hyzq3Jx3g==", - "cpu": [ - "riscv64" - ], + "cpu": ["riscv64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.0.tgz", "integrity": "sha512-VU/P/IODrNPasgZDLIFJmMiLGez+BN11DQWfTVlViJVabyF3JaeaJkP6teI8760f18BMGCQOW9gOmuzFaI1pUw==", - "cpu": [ - "s390x" - ], + "cpu": ["s390x"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.0.tgz", "integrity": "sha512-laQVRvdbKmjXuFA3ZiZj7+U24FcmoPlXEi2OyLfbpY2MW1oxLt9Au8q9eHd0x6Pw/Kw4oe9gwVXWwIf2PVqblg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.0.tgz", "integrity": "sha512-3wzKzduS7jzxqcOvy/ocU/gMR3/QrHEFLge5CD7Si9fyHuoXcidyYZ6jyx8OPYmCcGm3uKTUl+9jUSAY74Ln5A==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "os": ["linux"] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.0.tgz", "integrity": "sha512-jROwnI1+wPyuv696rAFHp5+6RFhXGGwgmgSfzE8e4xfit6oLRg7GyMArVUoM3ChS045OwWr9aTnU+2c1UdBMyw==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "os": ["win32"] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.0.tgz", "integrity": "sha512-duzweyup5WELhcXx5H1jokpr13i3BV9b48FMiikYAwk/MT1LrMYYk2TzenBd0jj4ivQIt58JWSxc19y4SvLP4g==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "os": ["win32"] }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.30.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.0.tgz", "integrity": "sha512-DYvxS0M07PvgvavMIybCOBYheyrqlui6ZQBHJs6GqduVzHSZ06TPPvlfvnYstjODHQ8UUXFwt5YE+h0jFI8kwg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "os": ["win32"] }, "node_modules/@supabase/auth-js": { "version": "2.67.3", @@ -927,9 +759,7 @@ "hasInstallScript": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/examples/auth/hono-full/src/client.tsx b/examples/auth/hono-full/src/client.tsx index 1c2a0d9c5d6..66f64c6eb02 100644 --- a/examples/auth/hono-full/src/client.tsx +++ b/examples/auth/hono-full/src/client.tsx @@ -1,29 +1,29 @@ -import { createBrowserClient } from '@supabase/ssr'; -import { hc } from 'hono/client'; -import { useEffect, useState } from 'hono/jsx'; -import { render } from 'hono/jsx/dom'; -import type { AppType } from '.'; +import { createBrowserClient } from '@supabase/ssr' +import { hc } from 'hono/client' +import { useEffect, useState } from 'hono/jsx' +import { render } from 'hono/jsx/dom' +import type { AppType } from '.' -const client = hc('/'); +const client = hc('/') const supabase = createBrowserClient( import.meta.env.VITE_SUPABASE_URL!, import.meta.env.VITE_SUPABASE_ANON_KEY! -); +) function App() { - const [user, setUser] = useState(null); + const [user, setUser] = useState(null) // Check client-side if user is logged in: useEffect(() => { supabase.auth.onAuthStateChange((event, session) => { - console.log('Auth event:', event); + console.log('Auth event:', event) if (event === 'SIGNED_OUT') { - setUser(null); + setUser(null) } else { - setUser(session?.user!); + setUser(session?.user!) } - }); - }, []); + }) + }, []) return ( <> @@ -35,7 +35,7 @@ function App() { - ); + ) } const UserDetailsButton = () => { - const [response, setResponse] = useState(null); + const [response, setResponse] = useState(null) const handleClick = async () => { - const response = await client.api.user.$get(); - const data = await response.json(); - const headers = Array.from(response.headers.entries()).reduce< - Record - >((acc, [key, value]) => { - acc[key] = value; - return acc; - }, {}); + const response = await client.api.user.$get() + const data = await response.json() + const headers = Array.from(response.headers.entries()).reduce>( + (acc, [key, value]) => { + acc[key] = value + return acc + }, + {} + ) const fullResponse = { url: response.url, status: response.status, headers, body: data, - }; - setResponse(JSON.stringify(fullResponse, null, 2)); - }; + } + setResponse(JSON.stringify(fullResponse, null, 2)) + } return (
    @@ -107,8 +103,8 @@ const UserDetailsButton = () => { {response &&
    {response}
    }
    - ); -}; + ) +} -const root = document.getElementById('root')!; -render(, root); +const root = document.getElementById('root')! +render(, root) diff --git a/examples/auth/hono-full/src/index.tsx b/examples/auth/hono-full/src/index.tsx index 5acaf4801f8..8560a3bcabf 100644 --- a/examples/auth/hono-full/src/index.tsx +++ b/examples/auth/hono-full/src/index.tsx @@ -1,42 +1,42 @@ -import { Hono } from 'hono'; -import { getSupabase, supabaseMiddleware } from './middleware/auth.middleware'; +import { Hono } from 'hono' +import { getSupabase, supabaseMiddleware } from './middleware/auth.middleware' -const app = new Hono(); -app.use('*', supabaseMiddleware()); +const app = new Hono() +app.use('*', supabaseMiddleware()) const routes = app.get('/api/user', async (c) => { - const supabase = getSupabase(c); - const { data, error } = await supabase.auth.getUser(); + const supabase = getSupabase(c) + const { data, error } = await supabase.auth.getUser() - if (error) console.log('error', error); + if (error) console.log('error', error) if (!data?.user) { return c.json({ message: 'You are not logged in.', - }); + }) } return c.json({ message: 'You are logged in!', userId: data.user, - }); -}); + }) +}) app.get('/signout', async (c) => { - const supabase = getSupabase(c); - await supabase.auth.signOut(); - console.log('Signed out server-side!'); - return c.redirect('/'); -}); + const supabase = getSupabase(c) + await supabase.auth.signOut() + console.log('Signed out server-side!') + return c.redirect('/') +}) app.get('/countries', async (c) => { - const supabase = getSupabase(c); - const { data, error } = await supabase.from('countries').select('*'); - if (error) console.log(error); - return c.json(data); -}); + const supabase = getSupabase(c) + const { data, error } = await supabase.from('countries').select('*') + if (error) console.log(error) + return c.json(data) +}) -export type AppType = typeof routes; +export type AppType = typeof routes app.get('/', (c) => { return c.html( @@ -44,10 +44,7 @@ app.get('/', (c) => { - + {import.meta.env.PROD ? (