diff --git a/.github/workflows/plugins-deploy-api-doc.yml b/.github/workflows/plugins-deploy-api-doc.yml index aaa1339c9e..1842a61b16 100644 --- a/.github/workflows/plugins-deploy-api-doc.yml +++ b/.github/workflows/plugins-deploy-api-doc.yml @@ -104,6 +104,23 @@ jobs: run: | sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml + - name: Add noindex header and robots.txt files for non-production environments + if: ${{ steps.vars.outputs.gh_ref != 'main' }} + working-directory: plugins + shell: bash + run: | + ASSETS_DIR="dist/doc" + + cat > "${ASSETS_DIR}/_headers" << 'EOF' + /* + X-Robots-Tag: noindex, nofollow + EOF + + cat > "${ASSETS_DIR}/robots.txt" << 'EOF' + User-agent: * + Disallow: / + EOF + - name: Deploy to Cloudflare Workers uses: cloudflare/wrangler-action@v3 with: diff --git a/.github/workflows/plugins-deploy-styles-doc.yml b/.github/workflows/plugins-deploy-styles-doc.yml index 1e2b39e74d..f8e43899b8 100644 --- a/.github/workflows/plugins-deploy-styles-doc.yml +++ b/.github/workflows/plugins-deploy-styles-doc.yml @@ -102,6 +102,23 @@ jobs: run: | sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-styles-doc.toml + - name: Add noindex header and robots.txt files for non-production environments + if: ${{ steps.vars.outputs.gh_ref != 'main' }} + working-directory: plugins + shell: bash + run: | + ASSETS_DIR="dist/apps/example-styles" + + cat > "${ASSETS_DIR}/_headers" << 'EOF' + /* + X-Robots-Tag: noindex, nofollow + EOF + + cat > "${ASSETS_DIR}/robots.txt" << 'EOF' + User-agent: * + Disallow: / + EOF + - name: Deploy to Cloudflare Workers uses: cloudflare/wrangler-action@v3 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e4021568ca..4ba57dde95 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,9 +28,55 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Check clojure code format + - name: Lint Common + working-directory: ./common run: | - ./scripts/lint + corepack enable; + corepack install; + pnpm install; + pnpm run check-fmt:clj + pnpm run check-fmt:js + pnpm run lint:clj + + - name: Lint Frontend + working-directory: ./frontend + run: | + corepack enable; + corepack install; + pnpm install; + pnpm run check-fmt:js + pnpm run check-fmt:clj + pnpm run check-fmt:scss + pnpm run lint:clj + pnpm run lint:js + pnpm run lint:scss + + - name: Lint Backend + working-directory: ./backend + run: | + corepack enable; + corepack install; + pnpm install; + pnpm run check-fmt + pnpm run lint + + - name: Lint Exporter + working-directory: ./exporter + run: | + corepack enable; + corepack install; + pnpm install; + pnpm run check-fmt + pnpm run lint + + - name: Lint Library + working-directory: ./library + run: | + corepack enable; + corepack install; + pnpm install; + pnpm run check-fmt + pnpm run lint test-common: name: "Common Tests" @@ -41,12 +87,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Run tests on JVM - working-directory: ./common - run: | - clojure -M:dev:test - - - name: Run tests on NODE + - name: Run tests working-directory: ./common run: | ./scripts/test diff --git a/.gitignore b/.gitignore index 224d199dc3..d0a13534b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,4 @@ .pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions -.pnpm-store *-init.clj *.css.json *.jar @@ -20,8 +13,6 @@ .nyc_output .rebel_readline_history .repl -.shadow-cljs -.pnpm-store/ /*.jpg /*.md /*.png @@ -35,6 +26,8 @@ /notes /playground/ /backend/*.md +!/backend/AGENTS.md +/backend/.shadow-cljs /backend/*.sql /backend/*.txt /backend/assets/ @@ -47,13 +40,13 @@ /backend/experiments /backend/scripts/_env.local /bundle* -/cd.md /clj-profiler/ /common/coverage /common/target -/deploy +/common/.shadow-cljs /docker/images/bundle* /exporter/target +/exporter/.shadow-cljs /frontend/.storybook/preview-body.html /frontend/.storybook/preview-head.html /frontend/playwright-report/ @@ -67,9 +60,9 @@ /frontend/storybook-static/ /frontend/target/ /frontend/test-results/ +/frontend/.shadow-cljs /other/ -/scripts/ -/telemetry/ +/nexus/ /tmp/ /vendor/**/target /vendor/svgclean/bundle*.js @@ -78,13 +71,11 @@ /library/*.zip /external /penpot-nitrate - -clj-profiler/ -node_modules /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ /render-wasm/target/ +/**/node_modules /**/.yarn/* /.pnpm-store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..9505d47698 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,499 @@ +# Penpot – Instructions + +## Architecture Overview + +Penpot is a full-stack design tool composed of several distinct components: + +| Component | Language | Role | +|-----------|----------|------| +| `frontend/` | ClojureScript + SCSS | Single-page React app (design editor) | +| `backend/` | Clojure (JVM) | HTTP/RPC server, PostgreSQL, Redis | +| `common/` | Cljc (shared Clojure/ClojureScript) | Data types, geometry, schemas, utilities | +| `exporter/` | ClojureScript (Node.js) | Headless Playwright-based export (SVG/PDF) | +| `render-wasm/` | Rust → WebAssembly | High-performance canvas renderer using Skia | +| `mcp/` | TypeScript | Model Context Protocol integration | +| `plugins/` | TypeScript | Plugin runtime and example plugins | + +The monorepo is managed with `pnpm` workspaces. The `manage.sh` +orchestrates cross-component builds. `run-ci.sh` defines the CI +pipeline. + +## Search Standards + +When searching code, always use `ripgrep` (rg) instead of grep if +available, as it respects `.gitignore` by default. + +If using grep, try to exclude node_modules and .shadow-cljs directories + + +## Build, Test & Lint Commands + +### Frontend (`cd frontend`) + +Run `./scripts/setup` for setup all dependencies. + + +```bash +# Build (Producution) +./scripts/build + +# Tests +pnpm run test # Build ClojureScript tests + run node target/tests/test.js + +# Lint +pnpm run lint:js # Linter for JS/TS +pnpm run lint:clj # Linter for CLJ/CLJS/CLJC +pnpm run lint:scss # Linter for SCSS + +# Check Code Formart +pnpm run check-fmt:clj # Format CLJ/CLJS/CLJC +pnpm run check-fmt:js # Format JS/TS +pnpm run check-fmt:scss # Format SCSS + +# Code Format (Automatic Formating) +pnpm run fmt:clj # Format CLJ/CLJS/CLJC +pnpm run fmt:js # Format JS/TS +pnpm run fmt:scss # Format SCSS +``` + +To run a focused ClojureScript unit test: edit +`test/frontend_tests/runner.cljs` to narrow the test suite, then `pnpm +run build:test && node target/tests/test.js`. + + +### Backend (`cd backend`) + +Run `pnpm install` for install all dependencies. + +```bash +# Run full test suite +pnpm run test + +# Run single namespace +pnpm run test --focus backend-tests.rpc-doc-test + +# Check Code Format +pnpm run check-fmt + +# Code Format (Automatic Formatting) +pnpm run fmt + +# Code Linter +pnpm run lint +``` + +Test config is in `backend/tests.edn`; test namespaces match +`.*-test$` under `test/` directory. You should not touch this file, +just use it for reference. + + +### Common (`cd common`) + +This contains code that should compile and run under different runtimes: JVM & JS so the commands are +separarated for each runtime. + +```bash +clojure -M:dev:test # Run full test suite under JVM +clojure -M:dev:test --focus backend-tests.my-ns-test # Run single namespace under JVM + +# Run full test suite under JS or JVM runtimes +pnpm run test:js +pnpm run test:jvm + +# Run single namespace (only on JVM) +pnpm run test:jvm --focus common-tests.my-ns-test + +# Lint +pnpm run lint:clj # Lint CLJ/CLJS/CLJC code + +# Check Format +pnpm run check-fmt:clj # Check CLJ/CLJS/CLJS code +pnpm run check-fmt:js # Check JS/TS code + +# Code Format (Automatic Formatting) +pnpm run fmt:clj # Check CLJ/CLJS/CLJS code +pnpm run fmt:js # Check JS/TS code +``` + +To run a focused ClojureScript unit test: edit +`test/common_tests/runner.cljs` to narrow the test suite, then `pnpm +run build:test && node target/tests/test.js`. + + +### Render-WASM (`cd render-wasm`) + +```bash +./test # Rust unit tests (cargo test) +./build # Compile to WASM (requires Emscripten) +cargo fmt --check +./lint --debug +``` + +## Key Conventions + +### Namespace Structure + +The backend, frontend and exporter are developed using clojure and +clojurescript and code is organized in namespaces. This is a general +overview of the available namespaces. + +**Backend:** +- `app.rpc.commands.*` – RPC command implementations (`auth`, `files`, `teams`, etc.) +- `app.http.*` – HTTP routes and middleware +- `app.db.*` – Database layer +- `app.tasks.*` – Background job tasks +- `app.main` – Integrant system setup and entrypoint +- `app.loggers` – Internal loggers (auditlog, mattermost, etc) (do not be confused with `app.common.loggin`) + +**Frontend:** +- `app.main.ui.*` – React UI components (`workspace`, `dashboard`, `viewer`) +- `app.main.data.*` – Potok event handlers (state mutations + side effects) +- `app.main.refs` – Reactive subscriptions (okulary lenses) +- `app.main.store` – Potok event store +- `app.util.*` – Utilities (DOM, HTTP, i18n, keyboard shortcuts) + +**Common:** +- `app.common.types.*` – Shared data types for shapes, files, pages using Malli schemas +- `app.common.schema` – Malli abstraction layer, exposes the most used functions from malli +- `app.common.geom.*` – Geometry and shape transformation helpers +- `app.common.data` – Generic helpers used around all application +- `app.common.math` – Generic math helpers used around all aplication +- `app.common.json` – Generic JSON encoding/decoding helpers +- `app.common.data.macros` – Performance macros used everywhere + + +### Backend RPC Commands + +The PRC methods are implement in a some kind of multimethod structure using +`app.util.serivices` namespace. All RPC methods are collected under `app.rpc` +namespace and exposed under `/api/rpc/command/`. The RPC method +accepts POST and GET requests indistinctly and uses `Accept` header for +negotiate the response encoding (which can be transit, the defaut or plain +json). It also accepts transit (defaut) or json as input, which should be +indicated using `Content-Type` header. + +This is an example: + +```clojure +(sv/defmethod ::my-command + {::rpc/auth true ;; requires auth + ::doc/added "1.18" + ::sm/params [:map ...] ;; malli input schema + ::sm/result [:map ...]} ;; malli output schema + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + ;; return a plain map or throw + {:id (uuid/next)}) +``` + +Look under `src/app/rpc/commands/*.clj` to see more examples. + + +### Frontend State Management (Potok) + +State is a single atom managed by a Potok store. Events implement protocols +(funcool/potok library): + +```clojure +(defn my-event + "doc string" + [data] + (ptk/reify ::my-event + ptk/UpdateEvent + (update [_ state] ;; synchronous state transition + (assoc state :key data)) + + ptk/WatchEvent + (watch [_ state stream] ;; async: returns an observable + (->> (rp/cmd! :some-rpc-command params) + (rx/map success-event) + (rx/catch error-handler))) + + ptk/EffectEvent + (effect [_ state _] ;; pure side effects (DOM, logging) + (dom/focus (dom/get-element "id"))))) +``` + +The state is located under `app.main.store` namespace where we have +the `emit!` function responsible of emiting events. + +Example: + +```cljs +(ns some.ns + (:require + [app.main.data.my-events :refer [my-event]] + [app.main.store :as st])) + +(defn on-click + [event] + (st/emit! (my-event))) +``` + +On `app.main.refs` we have reactive references which lookup into the main state +for just inner data or precalculated data. That references are very usefull but +should be used with care because, per example if we have complex operation, this +operation will be executed on each state change, and sometimes is better to have +simple references and use react `use-memo` for more granular memoization. + +Prefer helpers from `app.util.dom` instead of using direct dom calls, if no helper is +available, prefer adding a new helper for handling it and the use the +new helper. + + +### CSS (Modules Pattern) + +Styles are co-located with components. Each `.cljs` file has a corresponding +`.scss` file: + +```clojure +;; In the component namespace: +(require '[app.main.style :as stl]) + +;; In the render function: +[:div {:class (stl/css :container :active)}] + +;; Conditional: +[:div {:class (stl/css-case :some-class true :selected (= drawtool :rect))}] + +;; When you need concat an existing class: +[:div {:class [existing-class (stl/css-case :some-class true :selected (= drawtool :rect))]}] +``` + +### Integration tests (Playwright) + +Integration tests are developed under `frontend/playwright` directory, we use +mocks for remove communication with backend. + +The tests should be executed under `./frontend` directory: + +``` +cd frontend/ + +pnpm run test:e2e # Playwright e2e tests +pnpm run test:e2e --grep "pattern" # Single e2e test by pattern +``` + +Ensure everything installed with `./scripts/setup` script. + + +### Performance Macros (`app.common.data.macros`) + +Always prefer these macros over their `clojure.core` equivalents — they compile to faster JavaScript: + +```clojure +(dm/select-keys m [:a :b]) ;; ~6x faster than core/select-keys +(dm/get-in obj [:a :b :c]) ;; faster than core/get-in +(dm/str "a" "b" "c") ;; string concatenation +``` + +### Shared Code under Common (CLJC) + +Files in `common/src/app/common/` use reader conditionals to target both runtimes: + +```clojure +#?(:clj (import java.util.UUID) + :cljs (:require [cljs.core :as core])) +``` + +Both frontend and backend depend on `common` as a local library (`penpot/common +{:local/root "../common"}`). + + + +### Component Standards & Syntax (React & Rumext: mf/defc) + +The codebase contains various component patterns. When creating or refactoring +components, follow the Modern Syntax rules outlined below. + +1. The * Suffix Convention + +The most recent syntax uses a * suffix in the component name (e.g., +my-component*). This suffix signals the mf/defc macro to apply specific rules +for props handling and destructuring and optimization. + +2. Component Definition + +Modern components should use the following structure: + +```clj +(mf/defc my-component* + {::mf/wrap [mf/memo]} ;; Equivalent to React.memo + [{:keys [name on-click]}] ;; Destructured props + [:div {:class (stl/css :root) + :on-click on-click} + name]) +``` + +3. Hooks + +Use the mf namespace for hooks to maintain consistency with the macro's +lifecycle management. These are analogous to standard React hooks: + +```clj +(mf/use-state) ;; analogous to React.useState adapted to cljs semantics +(mf/use-effect) ;; analogous to React.useEffect +(mf/use-memo) ;; analogous to React.useMemo +(mf/use-fn) ;; analogous to React.useCallback +``` + +The `mf/use-state` in difference with React.useState, returns an atom-like +object, where you can use `swap!` or `reset!` for to perform an update and +`deref` for get the current value. + +You also has `mf/deref` hook (which does not follow the `use-` naming pattern) +and it's purpose is watch (subscribe to changes) on atom or derived atom (from +okulary) and get the current value. Is mainly used for subscribe to lenses +defined in `app.main.refs` or (private lenses defined in namespaces). + +Rumext also comes with improved syntax macros as alternative to `mf/use-effect` +and `mf/use-memo` functions. Examples: + + +Example for `mf/with-memo` macro: + +```clj +;; Using functions +(mf/use-effect + (mf/deps team-id) + (fn [] + (st/emit! (dd/initialize team-id)) + (fn [] + (st/emit! (dd/finalize team-id))))) + +;; The same effect but using mf/with-effect +(mf/with-effect [team-id] + (st/emit! (dd/initialize team-id)) + (fn [] + (st/emit! (dd/finalize team-id)))) +``` + +Example for `mf/with-memo` macro: + +``` +;; Using functions +(mf/use-memo + (mf/deps projects team-id) + (fn [] + (->> (vals projects) + (filterv #(= team-id (:team-id %)))))) + +;; Using the macro +(mf/with-memo [projects team-id] + (->> (vals projects) + (filterv #(= team-id (:team-id %))))) +``` + +Prefer using the macros for it syntax simplicity. + + +4. Component Usage (Hiccup Syntax) + +When invoking a component within Hiccup, always use the [:> component* props] +pattern. + +Requirements for props: + +- Must be a map literal or a symbol pointing to a JavaScript props object. +- To create a JS props object, use the `#js` literal or the `mf/spread-object` helper macro. + +Examples: + +```clj +;; Using object literal (no need of #js because macro already interprets it) +[:> my-component* {:data-foo "bar"}] + +;; Using object literal (no need of #js because macro already interprets it) +(let [props #js {:data-foo "bar" + :className "myclass"}] + [:> my-component* props]) + +;; Using the spread helper +(let [props (mf/spread-object base-props {:extra "data"})] + [:> my-component* props]) +``` + +4. Checklist + +- [ ] Does the component name end with *? + + +## Commit Format Guidelines + +Format: ` ` + +``` +:bug: Fix unexpected error on launching modal + +Optional body explaining the why. + +Signed-off-by: Fullname +``` + +**Subject rules:** imperative mood, capitalize first letter, no +trailing period, ≤ 80 characters. Add an entry to `CHANGES.md` if +applicable. + +**Code patches must include a DCO sign-off** (`git commit -s`). + +| Emoji | Emoji-Code | Use for | +|-------|------|---------| +| 🐛 | `:bug:` | Bug fix | +| ✨ | `:sparkles:` | Improvement | +| 🎉 | `:tada:` | New feature | +| ♻️ | `:recycle:` | Refactor | +| 💄 | `:lipstick:` | Cosmetic changes | +| 🚑 | `:ambulance:` | Critical bug fix | +| 📚 | `:books:` | Docs | +| 🚧 | `:construction:` | WIP | +| 💥 | `:boom:` | Breaking change | +| 🔧 | `:wrench:` | Config update | +| ⚡ | `:zap:` | Performance | +| 🐳 | `:whale:` | Docker | +| 📎 | `:paperclip:` | Other non-relevant changes | +| ⬆️ | `:arrow_up:` | Dependency upgrade | +| ⬇️ | `:arrow_down:` | Dependency downgrade | +| 🔥 | `:fire:` | Remove files or code | +| 🌐 | `:globe_with_meridians:` | Translations | + + +## SCSS Rules & Migration + +### General rules + +- Prefer CSS custom properties ( `margin: var(--sp-xs);`) instead of scss + variables and get the already defined properties from `_sizes.scss`. The SCSS + variables are allowed and still used, just prefer properties if they are + already defined. +- If a value isn't in the DS, use the `px2rem(n)` mixin: `@use "ds/_utils.scss" + as *; padding: px2rem(23);`. +- Do **not** create new SCSS variables for one-off values. +- Use physical directions with logical ones to support RTL/LTR naturally. + - ❌ `margin-left`, `padding-right`, `left`, `right`. + - ✅ `margin-inline-start`, `padding-inline-end`, `inset-inline-start`. +- Always use the `use-typography` mixin from `ds/typography.scss`. + - ✅ `@include t.use-typography("title-small");` +- Use `$br-*` for radius and `$b-*` for thickness from `ds/_borders.scss`. +- Use only tokens from `ds/colors.scss`. Do **NOT** use `design-tokens.scss` or + legacy color variables. +- Use mixins only those defined in`ds/mixins.scss`. Avoid legacy mixins like + `@include flexCenter;`. Write standard CSS (flex/grid) instead. + +### Syntax & Structure + +- Use the `@use` instead of `@import`. If you go to refactor existing SCSS file, + try to replace all `@import` with `@use`. Example: `@use "ds/_sizes.scss" as + *;` (Use `as *` to expose variables directly). +- Avoid deep selector nesting or high-specificity (IDs). Flatten selectors: + - ❌ `.card { .title { ... } }` + - ✅ `.card-title { ... }` +- Leverage component-level CSS variables for state changes (hover/focus) instead + of rewriting properties. + +### Checklist + +- [ ] No references to `common/refactor/` +- [ ] All `@import` converted to `@use` (only if refactoring) +- [ ] Physical properties (left/right) using logical properties (inline-start/end). +- [ ] Typography implemented via `use-typography()` mixin. +- [ ] Hardcoded pixel values wrapped in `px2rem()`. +- [ ] Selectors are flat (no deep nesting). diff --git a/CHANGES.md b/CHANGES.md index b872f0c4d6..639df82f29 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## 2.14.0 (Unreleased) ### :boom: Breaking changes & Deprecations + - Deprecate `PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE` in favour of `PENPOT_HTTP_SERVER_MAX_BODY_SIZE`. ### :sparkles: New features & Enhancements @@ -33,6 +34,9 @@ - Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128) - Fix 45 rotated board titles rendered incorrectly [Taiga #13306](https://tree.taiga.io/project/penpot/issue/13306) - Fix cannot apply second token after creation while shape is selected [Taiga #13513](https://tree.taiga.io/project/penpot/issue/13513) +- Fix error activating a set with invalid shadow token applied [Taiga #13528](https://tree.taiga.io/project/penpot/issue/13528) +- Fix component "broken" after variant switch [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984) +- Fix incorrect query for file versions [Github #8463](https://github.com/penpot/penpot/pull/8463) ## 2.13.3 @@ -47,7 +51,6 @@ - Fix modifying shapes by apply negative tokens to border radius [Taiga #13317](https://tree.taiga.io/project/penpot/issue/13317) - Fix arbitrary file read security issue on create-font-variant rpc method (https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2) - ## 2.13.1 ### :bug: Bugs fixed diff --git a/backend/AGENTS.md b/backend/AGENTS.md new file mode 100644 index 0000000000..f0b4a7314c --- /dev/null +++ b/backend/AGENTS.md @@ -0,0 +1,87 @@ +# backend – Agent Instructions + +Clojure service running on the JVM. Uses Integrant for dependency injection, PostgreSQL for storage, and Redis for messaging/caching. + +## Commands + +```bash +# REPL (primary dev workflow) +./scripts/repl # Start nREPL + load dev/user.clj utilities + +# Tests (Kaocha) +clojure -M:dev:test # Full suite +clojure -M:dev:test --focus backend-tests.my-ns-test # Single namespace + +# Lint / Format +pnpm run lint:clj +pnpm run fmt:clj +``` + +Test namespaces match `.*-test$` under `test/`. Config is in `tests.edn`. + +## Integrant System + +`src/app/main.clj` declares the system map. Each key is a component; +values are config maps with `::ig/ref` for dependencies. Components +implement `ig/init-key` / `ig/halt-key!`. + +From the REPL (`dev/user.clj` is auto-loaded): +```clojure +(start!) ; boot the system +(stop!) ; halt the system +(restart!) ; stop + reload namespaces + start +``` + +## RPC Commands + +All API calls: `POST /api/rpc/command/`. + +```clojure +(sv/defmethod ::my-command + {::rpc/auth true ;; requires authentication (default) + ::doc/added "1.18" + ::sm/params [:map ...] ;; malli input schema + ::sm/result [:map ...]} ;; malli output schema + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + ;; return a plain map; throw via ex/raise for errors + {:id (uuid/next)}) +``` + +Add new commands in `src/app/rpc/commands/`. + +## Database + +`app.db` wraps next.jdbc. Queries use a SQL builder that auto-converts kebab-case ↔ snake_case. + +```clojure +;; Query helpers +(db/get pool :table {:id id}) ; fetch one row (throws if missing) +(db/get* pool :table {:id id}) ; fetch one row (returns nil) +(db/query pool :table {:team-id team-id}) ; fetch multiple rows +(db/insert! pool :table {:name "x" :team-id id}) ; insert +(db/update! pool :table {:name "y"} {:id id}) ; update +(db/delete! pool :table {:id id}) ; delete +;; Transactions +(db/tx-run cfg (fn [{:keys [::db/conn]}] + (db/insert! conn :table row))) +``` + +Almost all methods on `app.db` namespace accepts `pool`, `conn` or +`cfg` as params. + +Migrations live in `src/app/migrations/` as numbered SQL files. They run automatically on startup. + +## Error Handling + +```clojure +(ex/raise :type :not-found + :code :object-not-found + :hint "File does not exist" + :context {:id file-id}) +``` + +Common types: `:not-found`, `:validation`, `:authorization`, `:conflict`, `:internal`. + +## Configuration + +`src/app/config.clj` reads `PENPOT_*` environment variables, validated with Malli. Access anywhere via `(cf/get :smtp-host)`. Feature flags: `(cf/flags :enable-smtp)`. diff --git a/backend/package.json b/backend/package.json index f3f4c18476..8ad7cd3c1d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,8 +19,9 @@ "ws": "^8.17.0" }, "scripts": { - "fmt:clj:check": "cljfmt check --parallel=false src/ test/", - "fmt:clj": "cljfmt fix --parallel=true src/ test/", - "lint:clj": "clj-kondo --parallel --lint src/" + "lint": "clj-kondo --parallel --lint ../common/src src/", + "check-fmt": "cljfmt check --parallel=true src/ test/", + "fmt": "cljfmt fix --parallel=true src/ test/", + "test": "clojure -M:dev:test" } } diff --git a/backend/resources/app/templates/error-report.v4.tmpl b/backend/resources/app/templates/error-report.v4.tmpl index beb5ebdb74..37a066a623 100644 --- a/backend/resources/app/templates/error-report.v4.tmpl +++ b/backend/resources/app/templates/error-report.v4.tmpl @@ -8,7 +8,6 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)