diff --git a/.github/workflows/label_prs.yml b/.github/workflows/label_prs.yml index 7bd90fd102e72..9e9ac01756e90 100644 --- a/.github/workflows/label_prs.yml +++ b/.github/workflows/label_prs.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - id: label - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 - name: Comment when api-deploy-required is auto-applied if: contains(steps.label.outputs.new-labels, 'api-deploy-required') diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 7ed452065a1a9..c517dc08bfe80 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -29,10 +29,10 @@ jobs: with: role-to-assume: ${{ secrets.PROD_AWS_ROLE }} aws-region: us-east-1 - - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: public.ecr.aws - - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/publish_image.yml b/.github/workflows/publish_image.yml index e40a023c52ccb..487fa98341634 100644 --- a/.github/workflows/publish_image.yml +++ b/.github/workflows/publish_image.yml @@ -46,7 +46,7 @@ jobs: - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -83,7 +83,7 @@ jobs: tags: | type=raw,value=${{ needs.settings.outputs.image_version }}_${{ env.arch }} - - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -115,7 +115,7 @@ jobs: steps: - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1050707fc7490..168b1191370c2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Label Stale Issues - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0 with: days-before-issue-stale: 30 stale-issue-message: 'After 30 days of inactivity, this issue has been marked as stale. Commenting (or other activity) will remove the stale label.' diff --git a/.github/workflows/studio-e2e-test.yml b/.github/workflows/studio-e2e-test.yml index 890ee7d2be283..9573189273d52 100644 --- a/.github/workflows/studio-e2e-test.yml +++ b/.github/workflows/studio-e2e-test.yml @@ -93,7 +93,7 @@ jobs: with: role-to-assume: ${{ secrets.PROD_AWS_ROLE }} aws-region: us-east-1 - - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 if: steps.filter.outputs.studio == 'true' && !github.event.pull_request.head.repo.fork with: registry: public.ecr.aws diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 196a9cb40e8ee..b3a88a149eab5 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -422,6 +422,11 @@ export const gettingstarted: NavMenuConstant = { url: '/guides/getting-started/quickstarts/ruby-on-rails' as `/${string}`, enabled: !jsOnly, }, + { + name: 'Spring Boot', + url: '/guides/getting-started/quickstarts/spring-boot' as `/${string}`, + enabled: !jsOnly, + }, { name: 'SolidJS', url: '/guides/getting-started/quickstarts/solidjs', diff --git a/apps/docs/content/guides/getting-started/quickstarts/spring-boot.mdx b/apps/docs/content/guides/getting-started/quickstarts/spring-boot.mdx new file mode 100644 index 0000000000000..cc0015e5fcf4b --- /dev/null +++ b/apps/docs/content/guides/getting-started/quickstarts/spring-boot.mdx @@ -0,0 +1,218 @@ +--- +title: 'Use Supabase with Spring Boot' +subtitle: 'Learn how to create a Spring Boot project and connect it to your Supabase project.' +breadcrumb: 'Framework Quickstarts' +--- + + + +## Prerequisites + +Before you begin, make sure you have: + +- Java 17 or later, which you can check with `java -version` +- `curl` and `unzip`, to download and extract the generated project + +## 1. Create a Spring Boot project + +Use [Spring Initializr](https://start.spring.io) to scaffold a new project with the Web, Spring Data JPA, and Postgres Driver dependencies. Run the following from the directory where you keep your projects. + +```bash +curl https://start.spring.io/starter.zip \ + -d dependencies=web,data-jpa,postgresql \ + -d type=maven-project \ + -d language=java \ + -d groupId=com.example \ + -d artifactId=instruments \ + -d name=instruments \ + -o instruments.zip +unzip instruments.zip -d instruments && cd instruments +``` + +## 2. Install Supabase's Agent Skills (optional) + +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. + +Install them so your AI coding agent can produce more accurate, reliable code using current Supabase patterns, such as authentication, server-side rendering, and database migrations, rather than relying solely on training data. + +To install, run the following command in the root of your project: + +```bash +npx skills add supabase/agent-skills +``` + +## 3. Set up the Postgres connection details + +Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely. + +When your project is up and running, navigate to its dashboard and click on [Connect](/dashboard/project/_?showConnect=true&method=session). + + + +The Transaction pooler (port `6543`) doesn't work as your app's main data source, because Spring Data JPA uses Hibernate, which relies on server-side prepared statements. Use the Session pooler, or the direct connection string if you're in an [IPv6 environment](/docs/guides/troubleshooting/supabase--your-network-ipv4-and-ipv6-compatibility-cHe3BP) or have the [IPv4 Add-On](/docs/guides/platform/ipv4-address). + + + +Under the **Session pooler** (port `5432`), select the **JDBC** tab and copy the connection string. Replace the password placeholder with your saved database password, and [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) any reserved characters it contains, such as `&`, `#`, `?`, or a space. + + + +You can reset your database password in your [Database Settings](/dashboard/project/_/database/settings) if you do not have it. + + + +The connection string contains your database password, and `application.properties` is committed with your project. Set the string as an environment variable instead, and set it the same way on whatever platform you deploy to. + +```bash +export SUPABASE_DB_URL='jdbc:postgresql://xxxx.pooler.supabase.com:5432/postgres?user=postgres.xxxx&password=[YOUR-PASSWORD]&sslmode=require' +``` + +The string you copied doesn't set `sslmode`, so add it. The driver defaults to `prefer`, which falls back to sending your data in plaintext if the encrypted attempt fails. You can also [enforce SSL](/docs/guides/platform/ssl-enforcement) on the database side. + +Then reference the variable, along with the driver, in `src/main/resources/application.properties`. + +```text name=src/main/resources/application.properties +spring.datasource.url=${SUPABASE_DB_URL} +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.hibernate.ddl-auto=update +``` + +If the app fails to start with `Unable to determine Dialect without JDBC metadata`, Hibernate couldn't open a connection at all. Look above that line in the logs for the real cause, most commonly `password authentication failed`. + +## 4. Change the default schema + +By default Hibernate creates tables in the `public` schema. We recommend changing this as Supabase exposes the `public` schema as a [data API](/docs/guides/api). + +Create the schema from the [Table Editor](/dashboard/project/_/editor) as your app will need it before start. Then point **Hibernate** at it in `application.properties`. + +```text name=src/main/resources/application.properties +spring.jpa.properties.hibernate.default_schema=app +``` + +## 5. Create an entity and repository + +Spring Data JPA maps Java classes to database tables. Create an `Instrument` entity in `src/main/java/com/example/instruments/Instrument.java`. With `spring.jpa.hibernate.ddl-auto=update` set, Hibernate creates the `instruments` table for you when the app starts. + +```java name=src/main/java/com/example/instruments/Instrument.java +package com.example.instruments; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "instruments") +public class Instrument { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + public Instrument() {} + + public Instrument(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +Create an `InstrumentRepository` interface in the same package. Extending `JpaRepository` gives you `findAll`, `save`, and other query methods without writing any implementation. + +```java name=src/main/java/com/example/instruments/InstrumentRepository.java +package com.example.instruments; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InstrumentRepository extends JpaRepository {} +``` + +## 6. Seed sample data + +Add a `CommandLineRunner` bean to `InstrumentsApplication.java` that saves some sample instruments the first time the app starts. + +```java name=src/main/java/com/example/instruments/InstrumentsApplication.java +package com.example.instruments; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class InstrumentsApplication { + + public static void main(String[] args) { + SpringApplication.run(InstrumentsApplication.class, args); + } + + @Bean + CommandLineRunner seedInstruments(InstrumentRepository instrumentRepository) { + return args -> { + if (instrumentRepository.count() == 0) { + instrumentRepository.save(new Instrument("violin")); + instrumentRepository.save(new Instrument("viola")); + instrumentRepository.save(new Instrument("cello")); + } + }; + } +} +``` + +## 7. Query data from the app + +Create an `InstrumentController` that fetches every row from the `instruments` table through the repository and returns it as JSON. + +```java name=src/main/java/com/example/instruments/InstrumentController.java +package com.example.instruments; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class InstrumentController { + + private final InstrumentRepository instrumentRepository; + + public InstrumentController(InstrumentRepository instrumentRepository) { + this.instrumentRepository = instrumentRepository; + } + + @GetMapping("/instruments") + public List getInstruments() { + return instrumentRepository.findAll(); + } +} +``` + +## 8. Start the app + +Run the Spring Boot app, and go to http://localhost:8080/instruments in your browser. You should see the list of instruments. + +```bash +./mvnw spring-boot:run +``` + +## Next steps + +- Set up [Auth](/docs/guides/auth) for your app +- Replace `ddl-auto` with [database migrations](/docs/guides/deployment/database-migrations) before going to production +- [Insert more data](/docs/guides/database/import-data) into your database +- Upload and serve static files using [Storage](/docs/guides/storage) diff --git a/apps/docs/data/ai-prompts.data.ts b/apps/docs/data/ai-prompts.data.ts index 74cf6930033e6..788b3acf364e5 100644 --- a/apps/docs/data/ai-prompts.data.ts +++ b/apps/docs/data/ai-prompts.data.ts @@ -201,6 +201,30 @@ database.new and run the instruments table SQL. Then: REFERENCE https://supabase.com/docs/guides/getting-started/quickstarts/solidjs.md`, + 'spring-boot': `Help me add Supabase to my Spring Boot project. Create a Supabase project at +database.new. Then: +1. Run \`curl https://start.spring.io/starter.zip -d dependencies=web,data-jpa,postgresql + -d type=maven-project -d language=java -d groupId=com.example -d artifactId=instruments + -d name=instruments -o instruments.zip\` and unzip it to scaffold the project. +2. Copy the JDBC connection string for the Session pooler (port 5432) from the Supabase + Connect panel and export it as a \`SUPABASE_DB_URL\` environment variable, so the + password stays out of source control. Set \`spring.datasource.url=\${SUPABASE_DB_URL}\` + and \`spring.datasource.driver-class-name\` in \`application.properties\`. Avoid the + Transaction pooler (port 6543) since Hibernate relies on prepared statements. +3. Set \`spring.jpa.hibernate.ddl-auto=update\` and + \`spring.jpa.properties.hibernate.default_schema\` in \`application.properties\`, so + Hibernate creates tables outside the \`public\` schema that Supabase exposes as a data API. +4. Create an \`Instrument\` JPA entity mapped to the \`instruments\` table with + \`@Table(name = "instruments")\`, and an \`InstrumentRepository\` extending + \`JpaRepository\`. +5. Add a \`CommandLineRunner\` bean to \`InstrumentsApplication\` that seeds the table + with a few instruments the first time the app starts. +6. Create an \`InstrumentController\` with a \`GET /instruments\` endpoint that returns + \`instrumentRepository.findAll()\`. +7. Run \`./mvnw spring-boot:run\` and open http://localhost:8080/instruments. + +REFERENCE +https://supabase.com/docs/guides/getting-started/quickstarts/spring-boot.md`, sveltekit: `Help me add Supabase to my SvelteKit project. Create a Supabase project at database.new and run the instruments table SQL. Then: 1. Run \`npx sv create my-app\` to scaffold the app. diff --git a/apps/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json index bb9dff36ebbd9..e19a374737311 100644 --- a/apps/studio/.github/eslint-rule-baselines.json +++ b/apps/studio/.github/eslint-rule-baselines.json @@ -2,7 +2,7 @@ "rules": { "react-hooks/exhaustive-deps": 160, "import/no-anonymous-default-export": 57, - "@tanstack/query/exhaustive-deps": 9, + "@tanstack/query/exhaustive-deps": 89, "@typescript-eslint/no-explicit-any": 875, "no-restricted-imports": 0, "no-restricted-exports": 193, @@ -208,11 +208,86 @@ "@tanstack/query/exhaustive-deps": { "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx": 1, "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 2, + "data/auth/index-worker-status-query.ts": 1, + "data/auth/user-query.ts": 1, + "data/auth/user-search-indexes-query.ts": 1, + "data/auth/users-count-query.ts": 1, + "data/auth/users-infinite-query.ts": 1, "data/branches/branch-diff-query.ts": 1, + "data/config/disk-breakdown-query.ts": 1, + "data/config/project-upgrade-status-query.ts": 1, + "data/database-cron-jobs/database-cron-job-query.ts": 1, + "data/database-cron-jobs/database-cron-jobs-count-estimate-query.ts": 1, + "data/database-cron-jobs/database-cron-jobs-count-query.ts": 1, + "data/database-cron-jobs/database-cron-jobs-infinite-query.ts": 1, + "data/database-cron-jobs/database-cron-jobs-minimal-infinite-query.ts": 1, + "data/database-cron-jobs/database-cron-jobs-runs-infinite-query.ts": 1, + "data/database-cron-jobs/database-cron-timezone-query.ts": 1, + "data/database-event-triggers/database-event-triggers-query.ts": 1, + "data/database-extensions/database-extensions-query.ts": 1, + "data/database-functions/database-functions-query.ts": 1, + "data/database-indexes/indexes-query.ts": 1, + "data/database-integrations/stripe/sync-state-query.ts": 1, + "data/database-policies/database-policies-query.ts": 1, + "data/database-publications/database-publications-query.ts": 1, + "data/database-queues/database-queue-messages-infinite-query.ts": 1, + "data/database-queues/database-queues-expose-postgrest-status-query.ts": 1, + "data/database-queues/database-queues-metrics-query.ts": 1, + "data/database-queues/database-queues-query.ts": 1, + "data/database-roles/database-roles-query.ts": 1, + "data/database-triggers/database-triggers-query.ts": 2, + "data/database/activity-query.ts": 1, + "data/database/constraints-query.ts": 1, + "data/database/database-size-query.ts": 1, "data/database/foreign-key-constraints-query.ts": 1, - "data/database/schemas-query.ts": 1, - "data/entity-types/entity-types-infinite-query.ts": 1, - "data/tables/tables-query.ts": 2 + "data/database/keywords-query.ts": 1, + "data/database/max-connections-query.ts": 1, + "data/database/migrations-query.ts": 1, + "data/database/retrieve-index-advisor-result-query.ts": 1, + "data/database/retrieve-index-from-select-query.ts": 1, + "data/database/schemas-query.ts": 2, + "data/database/supamonitor-enabled-query.ts": 1, + "data/database/table-columns-query.ts": 1, + "data/database/table-definition-query.ts": 1, + "data/database/table-index-advisor-query.ts": 1, + "data/database/view-definition-query.ts": 1, + "data/entity-types/entity-types-infinite-query.ts": 2, + "data/enumerated-types/enumerated-types-query.ts": 1, + "data/fdw/fdws-query.ts": 1, + "data/foreign-tables/foreign-tables-query.ts": 1, + "data/invoices/invoices-query.ts": 1, + "data/logs/unified-log-inspection-query.ts": 1, + "data/materialized-views/materialized-views-query.ts": 1, + "data/misc/get-default-region-query.ts": 1, + "data/oauth-server-apps/oauth-openid-configuration-query.ts": 1, + "data/organizations/organization-audit-logs-query.ts": 1, + "data/organizations/organization-billing-subscription-preview.ts": 1, + "data/organizations/organization-creation-preview.ts": 1, + "data/organizations/organization-credit-top-up-preview.ts": 1, + "data/pg-graphql/schema-comment-query.ts": 1, + "data/privileges/column-privileges-query.ts": 1, + "data/privileges/exposed-functions-query.ts": 1, + "data/privileges/exposed-tables-query.ts": 1, + "data/privileges/table-privileges-query.ts": 1, + "data/profile/profile-audit-logs-query.ts": 1, + "data/projects/project-detail-query.ts": 1, + "data/projects/project-type-generation-query.ts": 1, + "data/read-replicas/replica-lag-query.ts": 1, + "data/sql/ongoing-queries-query.ts": 1, + "data/storage/bucket-objects-infinite-query.ts": 1, + "data/storage/iceberg-namespace-tables-query.ts": 1, + "data/storage/public-buckets-with-select-policies-query.ts": 1, + "data/table-editor/table-editor-query.ts": 1, + "data/table-rows/table-rows-count-query.ts": 1, + "data/table-rows/table-rows-query.ts": 1, + "data/tables/table-names-query.ts": 1, + "data/tables/table-retrieve-query.ts": 1, + "data/tables/tables-query.ts": 4, + "data/usage/org-usage-query.ts": 1, + "data/usage/resource-warnings-query.ts": 1, + "data/vault/vault-secret-decrypted-value-query.ts": 1, + "data/vault/vault-secrets-query.ts": 1, + "data/views/views-query.ts": 1 }, "@typescript-eslint/no-explicit-any": { "components/grid/SupabaseGrid.tsx": 1, diff --git a/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx b/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx index 07a5a4aff6446..dc8a5e96c39eb 100644 --- a/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx @@ -143,7 +143,6 @@ export const FilterPopoverNew = ({ useEffect(() => { syncFromFilters() - // eslint-disable-next-line react-hooks/exhaustive-deps -- useEffectEvent fn intentionally not a dep (eslint-plugin-react-hooks v5 doesn't recognize stable useEffectEvent yet) }, [filters]) const columns = useMemo(() => snap.table?.columns ?? [], [snap.table?.columns]) diff --git a/apps/studio/components/grid/components/header/filter/FilterRow.tsx b/apps/studio/components/grid/components/header/filter/FilterRow.tsx index 6881a292dd062..09283e041eda1 100644 --- a/apps/studio/components/grid/components/header/filter/FilterRow.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterRow.tsx @@ -1,6 +1,6 @@ import { ChevronDown, X } from 'lucide-react' import { KeyboardEvent, memo } from 'react' -import { Button, Input } from 'ui' +import { Button, Input, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { FilterOperatorOptions } from './Filter.constants' import { DropdownControl } from '@/components/grid/components/common/DropdownControl' @@ -93,12 +93,18 @@ const FilterRow = ({ filter, filterIdx, onChange, onDelete, onKeyDown }: FilterR } onKeyDown={onKeyDown} /> - - -