Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions .cursor/rules/backend-java.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
description:
globs: app/server/**/*.java
alwaysApply: false
---
# Java / Spring Boot Backend Conventions

## Tech Stack

- Java 25, Spring Boot 3.5, Spring WebFlux (fully reactive), MongoDB, Maven
- Formatting: Palantir Java Format via `mvn spotless:apply` (auto-runs on pre-commit)

## Project Structure

Multi-module Maven project under `app/server/`:

```text
appsmith-server/ — Main server module (controllers, services, repositories, domains)
appsmith-interfaces/ — Shared contracts (models, exceptions, plugin interfaces)
appsmith-plugins/ — 25+ database/API connector plugins
appsmith-git/ — Git integration module
reactive-caching/ — Custom reactive caching framework
```

Within `appsmith-server`, packages are organized by feature domain, each with `ce/` and optionally `ce_compatible/` sub-packages.

## CE-EE Service Pattern (Mandatory for all extensible components)

### Three-tier inheritance

```text
Interface: ServiceCE → ServiceCECompatible → Service
Impl: ServiceCEImpl → ServiceCECompatibleImpl → ServiceImpl (@Service)
Package: com.x.services.ce com.x.services.ce_compatible com.x.services
```

### Rules

- `@Service` / `@Component` annotation on CE-Compatible and final `*Impl` classes (not on CE base classes)
- CE impl contains all business logic
- CE-Compatible is an empty pass-through in CE repo (EE overrides it for graceful degradation)
- Final `*Impl` is an empty pass-through in CE repo (EE overrides it for enterprise features)
- Use `protected` methods in CE impl to create override hooks for EE
- This pattern applies to: services, repositories, controllers, solutions, helpers, configurations

### When to create the CE-Compatible layer

Always. Even if there's no current EE override, create the empty scaffolding so EE can extend later without modifying CE files.

## Controller Conventions

```text
CE class (in controllers/ce/): All endpoint logic, NOT annotated as @RestController
EE class (in controllers/): @RestController + @RequestMapping, empty body, extends CE
```

- URL paths: `/api/v1/{resource}` — all paths defined in `UrlCE.java` constants
- Response: always `Mono<ResponseDTO<T>>` — every response wrapped in `ResponseDTO`
- `@JsonView(Views.Public.class)` on every endpoint method
- Zero business logic in controllers — delegate entirely to services
- No error handling in controllers — handled by global `@ControllerAdvice` (`GlobalExceptionHandler`)

## Repository Conventions

Dual hierarchy:
- `BaseRepository` (extends `ReactiveMongoRepository`) — standard CRUD
- `AppsmithRepository` (custom) — ACL-aware queries via `queryBuilder()` fluent API

```java
queryBuilder()
.criteria(Bridge.equal(Application.Fields.workspaceId, workspaceId))
.permission(aclPermission)
.all();
```

- No `@Query` annotations — use the `Bridge` query builder exclusively
- Field references via Lombok `@FieldNameConstants` (`Fields` inner class)
- Soft deletes: `archive()` sets `deletedAt`; queries auto-filter deleted records

## Domain / DTO Conventions

Hierarchy: `BaseDomain` (id, policyMap, timestamps) → `GitSyncedDomain` → `RefAwareDomain`

- `@Getter`, `@Setter`, `@ToString` on domains — **not** `@Data` (avoids generated equals/hashCode)
- `@FieldNameConstants` on all domains/DTOs for type-safe field references
- `@JsonView` annotations control API visibility (`Views.Public.class`, `Views.Internal.class`, `Git.class`, `FromRequest.class`)
- `@Transient` for computed fields not persisted to MongoDB
- Domains/DTOs also follow CE-EE split: `ActionDTO extends ActionCE_DTO`

## Dependency Injection

- Constructor-based injection exclusively (no `@Autowired` on fields)
- Use `@RequiredArgsConstructor` or `@AllArgsConstructor` (Lombok)
- `@Lazy` to break circular dependencies
- `@Autowired(required = false)` for optional dependencies

## Reactive Patterns

All service methods return `Mono<T>` or `Flux<T>`. Key patterns:

- `.switchIfEmpty(Mono.error(new AppsmithException(...)))` — standard "not found"
- `Mono.cache()` — prevent duplicate subscriptions when a Mono feeds multiple operators
- `Mono.zip()` / `.zipWith()` / `.zipWhen()` — combine parallel or dependent operations
- `Mono.defer()` — lazy evaluation inside `.switchIfEmpty()` and `.then()`
- `.flatMap()` chains — primary mechanism for sequential reactive operations
- `.onErrorResume()` — graceful degradation
- `.onErrorMap()` — translate errors to `AppsmithException`
- Never block reactive streams — no `.block()` in production code

## Error Handling

- `AppsmithException` wraps `AppsmithError` enum (100+ entries with HTTP status, error code `AE-{DOMAIN}-{NUMBER}`, message template)
- Throw via `new AppsmithException(AppsmithError.SOME_ERROR, args...)`
- Global handler (`GlobalExceptionHandler`) catches and returns `ResponseDTO<ErrorDTO>`
- Plugin errors: `AppsmithPluginException` wraps `BasePluginError`

## Feature Flags

- `@FeatureFlagged(featureFlagName = FeatureFlagEnum.XXX)` on EE methods
- Spring AOP `@Around` advice dispatches: flag ON → EE method; flag OFF → superclass (CE-Compatible/CE)
- For reactive methods: aspect uses `flatMap` on the flag check Mono
- For synchronous methods: requires `organizationId` parameter; reads from in-memory org cache
- New flags: add to `FeatureFlagEnum` in `appsmith-interfaces`
- Flag naming: `release_*` (rollout), `license_*` (paid), `rollout_*` (gradual), `ab_*` (experiment)

## Custom Annotations

| Annotation | Purpose |
|------------|---------|
| `@FeatureFlagged` | Controls method execution via feature flag + AOP |
| `@Encrypted` | Marks fields for at-rest encryption |
| `@Cache` / `@CacheEvict` | Reactive method result caching / eviction |
| `@DistributedLock` | Distributed lock with TTL (default 5 min) |
| `@RateLimit` | Rate-limits API endpoints |

## Build Commands

```bash
cd app/server && mvn spotless:apply # Format code
cd app/server && mvn spotless:check # Check formatting only
cd app/server && ./build.sh -DskipTests # Build without tests
cd app/server && ./build.sh -DskipTests -T 8 # Build without tests (8 threads)
```
57 changes: 57 additions & 0 deletions .cursor/rules/bug-fix-verification.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
description: Checklist for fixing bugs, debugging issues, and verifying bug fixes
globs:
alwaysApply: false
---
# Bug Fix Workflow (TDD Approach)

## 1. Understand

- Reproduce the issue locally and understand the bug report
- Identify root cause through code exploration — fix the cause, not symptoms
- Check if there's a related issue/ticket to reference

## 2. Write Failing Test(s)

Write test(s) that demonstrate the bug before writing any fix code.
- Server: add test in the relevant `*Test.java` class
- Client: add test in the relevant `*.test.ts(x)` file
- Test both CE and EE paths if the bug touches feature-flagged code

## 3. Verify RED

Run the test(s) and confirm they **FAIL**:
```bash
# Server
cd app/server && source envs/test.env.example && mvn test -pl appsmith-server -Dtest=ClassName

# Client
cd app/client && yarn test:jest -- --testPathPattern=<pattern>
```

If the test passes without any fix, it does not cover the bug. Rewrite the test.

## 4. Implement Fix

- Address the root cause with minimal, focused changes
- Follow the CE-EE pattern if the fix touches extensible components
- No hardcoded values, no debug logging (`console.log`, `System.out.println`)
- Use safe property access (optional chaining or lodash/get for nested objects)
- Handle edge cases and null/undefined gracefully

## 5. Verify GREEN

Run the same test(s) and confirm they now **PASS**. Use the same commands from step 3.

## 6. Sanity Check

Run closely related tests to catch obvious regressions. Do NOT run the full test suite locally — that's what CI is for.
- Find test classes in the same package/module as the changed code
- Run those specific tests to verify nothing adjacent broke
- Full regression is validated by CI after push

## Before Committing

- Formatting handled automatically by Husky pre-commit hooks
- PR title: `fix(scope): description`
- Reference the issue/ticket in PR description
149 changes: 149 additions & 0 deletions .cursor/rules/client-unit-tests.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
description:
globs: app/client/src/**/*.test.ts,app/client/src/**/*.test.tsx
alwaysApply: false
---
# Client Unit Test Conventions (Jest)

## Framework

- Jest 29 + React Testing Library (no Enzyme, no snapshot testing)
- `@testing-library/react` for rendering/querying
- `@testing-library/react` also provides `renderHook` for hook testing
- `@testing-library/user-event` for user interaction simulation
- `@testing-library/jest-dom` for DOM matchers
- MSW (Mock Service Worker) for API mocking
- `redux-mock-store` for Redux store mocking
- Test timeout: 9000ms

## Running Tests

```bash
cd app/client && yarn test:unit # All unit tests with coverage
cd app/client && yarn test:jest -- --testPathPattern=<pattern> # Specific tests (watch mode)
```

## Test Setup (Automatic)

Configured in `test/setup.ts`:
- MSW server starts `beforeAll`, resets handlers `afterEach`, closes `afterAll`
- Polyfills: `crypto`, `TextDecoder`, `ReadableStream`, `structuredClone`
- Mocks: `IntersectionObserver`, `ResizeObserver`, `scrollTo`

## Test Patterns

### Component test (with Redux)

```typescript
import { render, screen } from "test/testUtils";

describe("MyComponent", () => {
it("renders correctly with given props", () => {
render(<MyComponent label="test" />, {
initialState: store.getState(),
url: "/app/page-123",
});

expect(screen.getByText("test")).toBeInTheDocument();
});
});
```

The custom `render` from `test/testUtils` wraps components in `<BrowserRouter>`, `<Provider store>`, and `<ThemeProvider>`.

### Component test (simple, no Redux)

```typescript
import { render, screen } from "@testing-library/react";

it("renders label", () => {
render(<Button label="Click me" />);
expect(screen.getByText("Click me")).toBeInTheDocument();
});
```

### Hook test

```typescript
import { renderHook, act } from "@testing-library/react";

it("returns updated value", () => {
const { result } = renderHook(() => useMyHook(initialValue));

act(() => { result.current.update(newValue); });

expect(result.current.value).toBe(newValue);
});
```

### Saga test (generator stepping)

```typescript
import { runSaga } from "redux-saga";

it("dispatches success on valid input", async () => {
const dispatched: any[] = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action), getState: () => mockState },
mySaga,
{ type: ReduxActionTypes.TRIGGER, payload: input },
).toPromise();

expect(dispatched).toContainEqual({ type: ReduxActionTypes.SUCCESS, payload: expected });
});
```

### Parameterized test

```typescript
test.each([
["input1", "expected1"],
["input2", "expected2"],
])("converts %s to %s", (input, expected) => {
expect(transform(input)).toBe(expected);
});
```

### Async assertions

```typescript
import { waitFor } from "@testing-library/react";

await waitFor(() => {
expect(screen.getByText("loaded")).toBeInTheDocument();
});
```

## Mocking Patterns

### Module mock with partial override

```typescript
jest.mock("ee/utils/someUtil", () => ({
...jest.requireActual("ee/utils/someUtil"),
specificFunction: jest.fn().mockReturnValue(mockValue),
}));
```

### MSW API mock

```typescript
import { rest } from "msw";
import { server } from "test/__mocks__/server";

beforeEach(() => {
server.use(
rest.get("/api/v1/resource/:id", (req, res, ctx) =>
res(ctx.json({ responseMeta: { status: 200 }, data: mockData })),
),
);
});
```

## Key Conventions

- Always import from `"ee/..."` in tests too — never from `"ce/"` directly
- Use `test/testUtils` `render` for Redux-connected components; `@testing-library/react` for pure components
- Test factories in `test/factories/` for deterministic widget DSL data
- Callback mocking: `jest.fn()`, verify with `toHaveBeenCalledWith(...)`
- Assertions: `expect(x).toEqual(y)` for deep equality, `expect(x).toBe(y)` for primitives
31 changes: 31 additions & 0 deletions .cursor/rules/commit-conventions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
description:
globs:
alwaysApply: true
---
# Commit & PR Conventions

## PR Title Format (Conventional Commits)

`type(scope): description`

**Types:** feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert

**Examples:**
- `feat(widgets): add new table widget capabilities`
- `fix(auth): resolve login redirect issue`
- `refactor(api): simplify error handling logic`

## Commit Message Style

- Use descriptive messages with issue reference when applicable
- Verb-prefixed: "Add ...", "Fix ...", "Update ...", "Remove ..."

## Pre-commit Checks (Automatic via Husky)

When server files are staged → `mvn spotless:apply` runs automatically
When client files are staged → `eslint --fix --cache` + `prettier --write --cache` run via lint-staged
All staged files → `gitleaks` secret scanning runs automatically
Pre-commit checks are skipped for merge commits.

Do not manually run formatting before commit — Husky handles it.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading