699 lines
18 KiB
Markdown
699 lines
18 KiB
Markdown
|
|
# Appsmith Technical Details
|
||
|
|
|
||
|
|
This document provides in-depth technical information about the Appsmith codebase, focusing on implementation details, design patterns, and technologies used. This information will help Cursor AI better understand the code at a deeper level.
|
||
|
|
|
||
|
|
## Technology Stack
|
||
|
|
|
||
|
|
### Frontend
|
||
|
|
- **Framework**: React 17+
|
||
|
|
- **State Management**: Redux with Redux-Saga
|
||
|
|
- **Language**: TypeScript 4+
|
||
|
|
- **Styling**: Styled Components with Tailwind CSS
|
||
|
|
- **Build Tool**: Webpack
|
||
|
|
- **Testing**: Jest, React Testing Library, Cypress
|
||
|
|
- **Form Management**: Formik
|
||
|
|
- **API Client**: Axios
|
||
|
|
- **UI Components**: Custom component library
|
||
|
|
|
||
|
|
### Backend
|
||
|
|
- **Framework**: Spring Boot 2.x
|
||
|
|
- **Language**: Java 11+
|
||
|
|
- **Database**: MongoDB
|
||
|
|
- **Reactive Programming**: Project Reactor
|
||
|
|
- **Security**: Spring Security
|
||
|
|
- **API Documentation**: Swagger/OpenAPI
|
||
|
|
- **Caching**: Redis
|
||
|
|
|
||
|
|
## Key Frontend Implementation Details
|
||
|
|
|
||
|
|
### State Management
|
||
|
|
|
||
|
|
The application uses Redux with a sophisticated structure:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Example action
|
||
|
|
export const fetchDatasources = (applicationId: string) => ({
|
||
|
|
type: ReduxActionTypes.FETCH_DATASOURCES,
|
||
|
|
payload: { applicationId },
|
||
|
|
});
|
||
|
|
|
||
|
|
// Example reducer
|
||
|
|
const datasourceReducer = (state = initialState, action: ReduxAction<any>) => {
|
||
|
|
switch (action.type) {
|
||
|
|
case ReduxActionTypes.FETCH_DATASOURCES_SUCCESS:
|
||
|
|
return { ...state, list: action.payload };
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Example saga
|
||
|
|
function* fetchDatasourcesSaga(action: ReduxAction<{ applicationId: string }>) {
|
||
|
|
try {
|
||
|
|
const response = yield call(DatasourcesApi.fetchDatasources, action.payload.applicationId);
|
||
|
|
yield put({
|
||
|
|
type: ReduxActionTypes.FETCH_DATASOURCES_SUCCESS,
|
||
|
|
payload: response.data,
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
yield put({
|
||
|
|
type: ReduxActionTypes.FETCH_DATASOURCES_ERROR,
|
||
|
|
payload: { error },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Widget System
|
||
|
|
|
||
|
|
Widgets are the building blocks of the application UI. They follow a standard structure:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export type WidgetProps = {
|
||
|
|
widgetId: string;
|
||
|
|
type: string;
|
||
|
|
widgetName: string;
|
||
|
|
parentId?: string;
|
||
|
|
renderMode: RenderMode;
|
||
|
|
version: number;
|
||
|
|
// ...other properties
|
||
|
|
};
|
||
|
|
|
||
|
|
export default class ButtonWidget extends BaseWidget<ButtonWidgetProps, WidgetState> {
|
||
|
|
static getPropertyPaneConfig() {
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
sectionName: "General",
|
||
|
|
children: [
|
||
|
|
{
|
||
|
|
propertyName: "text",
|
||
|
|
label: "Label",
|
||
|
|
controlType: "INPUT_TEXT",
|
||
|
|
// ...
|
||
|
|
},
|
||
|
|
// ...other properties
|
||
|
|
],
|
||
|
|
},
|
||
|
|
// ...other sections
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
getPageView() {
|
||
|
|
return (
|
||
|
|
<ButtonComponent
|
||
|
|
// ...props
|
||
|
|
onClick={this.handleClick}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
handleClick = () => {
|
||
|
|
if (this.props.onClick) {
|
||
|
|
super.executeAction({
|
||
|
|
triggerPropertyName: "onClick",
|
||
|
|
dynamicString: this.props.onClick,
|
||
|
|
event: {
|
||
|
|
type: EventType.ON_CLICK,
|
||
|
|
// ...
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Property Pane System
|
||
|
|
|
||
|
|
The property pane is dynamically generated based on the widget configuration:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export const PropertyPaneView = (props: PropertyPaneViewProps) => {
|
||
|
|
const { config, panel } = props;
|
||
|
|
|
||
|
|
// Render property sections
|
||
|
|
return (
|
||
|
|
<PropertyPaneContainer>
|
||
|
|
{config.map((section) => (
|
||
|
|
<PropertySection key={section.sectionName} title={section.sectionName}>
|
||
|
|
{section.children.map((property) => (
|
||
|
|
<PropertyControl
|
||
|
|
key={property.propertyName}
|
||
|
|
propertyName={property.propertyName}
|
||
|
|
controlType={property.controlType}
|
||
|
|
// ...other props
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</PropertySection>
|
||
|
|
))}
|
||
|
|
</PropertyPaneContainer>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Data Binding
|
||
|
|
|
||
|
|
The app uses a JS evaluation engine to bind data to widgets:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export function evaluateDynamicValue(
|
||
|
|
dynamicValue: string,
|
||
|
|
data: Record<string, unknown>,
|
||
|
|
): any {
|
||
|
|
// Set up execution environment
|
||
|
|
const scriptToEvaluate = `
|
||
|
|
function evaluation() {
|
||
|
|
const $ = ${JSON.stringify(data)};
|
||
|
|
try {
|
||
|
|
return ${dynamicValue};
|
||
|
|
} catch (e) {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
evaluation();
|
||
|
|
`;
|
||
|
|
|
||
|
|
try {
|
||
|
|
return eval(scriptToEvaluate);
|
||
|
|
} catch (e) {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### API Integration
|
||
|
|
|
||
|
|
The API client is set up with Axios and handles authentication:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const axiosInstance = axios.create({
|
||
|
|
baseURL: "/api/v1",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
// Request interceptor for adding auth token
|
||
|
|
axiosInstance.interceptors.request.use((config) => {
|
||
|
|
const token = localStorage.getItem("AUTH_TOKEN");
|
||
|
|
if (token) {
|
||
|
|
// Basic validation - check if token is a valid JWT format
|
||
|
|
if (token.split('.').length === 3) {
|
||
|
|
config.headers.Authorization = `Bearer ${token}`;
|
||
|
|
} else {
|
||
|
|
// Handle invalid token - could log user out or refresh token
|
||
|
|
store.dispatch(refreshToken());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return config;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Response interceptor for handling errors
|
||
|
|
axiosInstance.interceptors.response.use(
|
||
|
|
(response) => response,
|
||
|
|
(error) => {
|
||
|
|
if (error.response && error.response.status === 401) {
|
||
|
|
// Handle unauthorized
|
||
|
|
store.dispatch(logoutUser());
|
||
|
|
}
|
||
|
|
return Promise.reject(error);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Key Backend Implementation Details
|
||
|
|
|
||
|
|
### Repository Pattern
|
||
|
|
|
||
|
|
Mongo repositories use reactive programming:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Repository
|
||
|
|
public interface DatasourceRepository extends ReactiveMongoRepository<Datasource, String> {
|
||
|
|
Mono<Datasource> findByNameAndOrganizationId(String name, String organizationId);
|
||
|
|
Flux<Datasource> findAllByOrganizationId(String organizationId);
|
||
|
|
Mono<Long> countByNameAndOrganizationId(String name, String organizationId);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Service Layer
|
||
|
|
|
||
|
|
Services handle business logic:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Service
|
||
|
|
@RequiredArgsConstructor
|
||
|
|
public class DatasourceServiceImpl implements DatasourceService {
|
||
|
|
private final DatasourceRepository repository;
|
||
|
|
private final PluginService pluginService;
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public Mono<Datasource> create(Datasource datasource) {
|
||
|
|
return repository.save(datasource)
|
||
|
|
.flatMap(saved -> pluginService.getById(datasource.getPluginId())
|
||
|
|
.map(plugin -> {
|
||
|
|
saved.setPlugin(plugin);
|
||
|
|
return saved;
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Other methods...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Controller Layer
|
||
|
|
|
||
|
|
Controllers expose REST APIs:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@RestController
|
||
|
|
@RequestMapping("/api/v1/datasources")
|
||
|
|
@RequiredArgsConstructor
|
||
|
|
public class DatasourceController {
|
||
|
|
private final DatasourceService service;
|
||
|
|
|
||
|
|
@PostMapping
|
||
|
|
public Mono<ResponseDTO<Datasource>> create(@RequestBody DatasourceDTO dto) {
|
||
|
|
Datasource datasource = new Datasource();
|
||
|
|
BeanUtils.copyProperties(dto, datasource);
|
||
|
|
|
||
|
|
return service.create(datasource)
|
||
|
|
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Other endpoints...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Security Configuration
|
||
|
|
|
||
|
|
Spring Security setup:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Configuration
|
||
|
|
@EnableWebFluxSecurity
|
||
|
|
@EnableReactiveMethodSecurity
|
||
|
|
public class SecurityConfig {
|
||
|
|
|
||
|
|
@Bean
|
||
|
|
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||
|
|
return http
|
||
|
|
.csrf().disable()
|
||
|
|
.formLogin().disable()
|
||
|
|
.httpBasic().disable()
|
||
|
|
.authorizeExchange()
|
||
|
|
.pathMatchers("/api/v1/public/**").permitAll()
|
||
|
|
.pathMatchers("/api/v1/auth/**").permitAll()
|
||
|
|
.anyExchange().authenticated()
|
||
|
|
.and()
|
||
|
|
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||
|
|
.build();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Other beans...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Query Execution
|
||
|
|
|
||
|
|
Action execution is handled in a structured way:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Service
|
||
|
|
@RequiredArgsConstructor
|
||
|
|
public class ActionExecutionServiceImpl implements ActionExecutionService {
|
||
|
|
private final ActionExecutorFactory executorFactory;
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public Mono<ActionExecutionResult> executeAction(ActionDTO action) {
|
||
|
|
ActionExecutor executor = executorFactory.getExecutor(action.getPluginType());
|
||
|
|
|
||
|
|
if (executor == null) {
|
||
|
|
return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_PLUGIN_ACTION));
|
||
|
|
}
|
||
|
|
|
||
|
|
return executor.execute(action);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Advanced Patterns
|
||
|
|
|
||
|
|
### Code Splitting
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Lazy loading of components
|
||
|
|
const ApplicationPage = React.lazy(() => import("pages/Applications"));
|
||
|
|
const EditorPage = React.lazy(() => import("pages/Editor"));
|
||
|
|
|
||
|
|
// Router setup
|
||
|
|
const routes = [
|
||
|
|
{
|
||
|
|
path: "/applications",
|
||
|
|
component: ApplicationPage,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
path: "/app/editor/:applicationId/:pageId",
|
||
|
|
component: EditorPage,
|
||
|
|
},
|
||
|
|
// ...
|
||
|
|
];
|
||
|
|
```
|
||
|
|
|
||
|
|
### Plugin System
|
||
|
|
|
||
|
|
The plugin system allows extensibility:
|
||
|
|
|
||
|
|
```java
|
||
|
|
public interface PluginExecutor<T, U> {
|
||
|
|
Mono<ActionExecutionResult> execute(T connection, U datasourceConfiguration, Object executeActionDTO);
|
||
|
|
Mono<T> datasourceCreate(U datasourceConfiguration);
|
||
|
|
void datasourceDestroy(T connection);
|
||
|
|
Set<String> getHintMessages(U datasourceConfiguration);
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Reactive Caching
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Service
|
||
|
|
public class CacheableRepositoryHelper {
|
||
|
|
private final Map<String, Cache<String, Object>> cacheMap = new ConcurrentHashMap<>();
|
||
|
|
|
||
|
|
public <T> Mono<T> fetchFromCache(String cacheName, String key, Supplier<Mono<T>> fetchFunction) {
|
||
|
|
Cache<String, Object> cache = cacheMap.computeIfAbsent(cacheName, k ->
|
||
|
|
Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).build());
|
||
|
|
|
||
|
|
Object cachedValue = cache.getIfPresent(key);
|
||
|
|
if (cachedValue != null) {
|
||
|
|
return Mono.just((T) cachedValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
return fetchFunction.get()
|
||
|
|
.doOnNext(value -> cache.put(key, value));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Action Collection System
|
||
|
|
|
||
|
|
Actions are grouped into collections for better organization:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Document
|
||
|
|
public class ActionCollection {
|
||
|
|
@Id
|
||
|
|
private String id;
|
||
|
|
private String name;
|
||
|
|
private String applicationId;
|
||
|
|
private String organizationId;
|
||
|
|
private String pageId;
|
||
|
|
private List<ActionDTO> actions;
|
||
|
|
private List<String> actionIds;
|
||
|
|
private String body;
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Common Code Patterns
|
||
|
|
|
||
|
|
### Error Handling
|
||
|
|
|
||
|
|
Frontend error handling:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Global error boundary
|
||
|
|
export class AppErrorBoundary extends React.Component<{}, { hasError: boolean }> {
|
||
|
|
constructor(props: {}) {
|
||
|
|
super(props);
|
||
|
|
this.state = { hasError: false };
|
||
|
|
}
|
||
|
|
|
||
|
|
static getDerivedStateFromError() {
|
||
|
|
return { hasError: true };
|
||
|
|
}
|
||
|
|
|
||
|
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||
|
|
logError(error, errorInfo);
|
||
|
|
}
|
||
|
|
|
||
|
|
render() {
|
||
|
|
if (this.state.hasError) {
|
||
|
|
return <ErrorPage />;
|
||
|
|
}
|
||
|
|
return this.props.children;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Backend error handling:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@ExceptionHandler(AppsmithException.class)
|
||
|
|
public Mono<ResponseEntity<ResponseDTO<Object>>> handleAppsmithException(AppsmithException exception) {
|
||
|
|
log.error("Application error: {}", exception.getMessage(), exception);
|
||
|
|
|
||
|
|
ResponseDTO<Object> response = new ResponseDTO<>(
|
||
|
|
exception.getHttpStatus().value(),
|
||
|
|
null,
|
||
|
|
new ErrorDTO(exception.getAppErrorCode(), exception.getMessage())
|
||
|
|
);
|
||
|
|
|
||
|
|
return Mono.just(ResponseEntity
|
||
|
|
.status(exception.getHttpStatus())
|
||
|
|
.body(response));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Validation
|
||
|
|
|
||
|
|
Frontend validation:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const validateWidgetName = (widgetName: string) => {
|
||
|
|
const nameRegex = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
||
|
|
|
||
|
|
if (!nameRegex.test(widgetName)) {
|
||
|
|
return "Widget name must start with a letter and can contain only letters, numbers, and underscore";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (widgetName.length > 30) {
|
||
|
|
return "Widget name must be less than 30 characters";
|
||
|
|
}
|
||
|
|
|
||
|
|
return undefined;
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
Backend validation:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Validated
|
||
|
|
@Service
|
||
|
|
public class UserServiceImpl implements UserService {
|
||
|
|
@Override
|
||
|
|
public Mono<User> create(@Valid UserDTO userDTO) {
|
||
|
|
// Implementation
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class UserDTO {
|
||
|
|
@NotBlank(message = "Email is mandatory")
|
||
|
|
@Email(message = "Invalid email format")
|
||
|
|
private String email;
|
||
|
|
|
||
|
|
@NotBlank(message = "Password is mandatory")
|
||
|
|
@Size(min = 8, message = "Password must be at least 8 characters")
|
||
|
|
private String password;
|
||
|
|
|
||
|
|
// Other fields...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Internationalization
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// i18n setup
|
||
|
|
const i18n = createI18n({
|
||
|
|
locale: getBrowserLocale(),
|
||
|
|
messages: {
|
||
|
|
en: enMessages,
|
||
|
|
fr: frMessages,
|
||
|
|
// Other languages...
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
// Usage in components
|
||
|
|
const MyComponent = () => {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<h1>{t('welcome.title')}</h1>
|
||
|
|
<p>{t('welcome.message')}</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
## Enterprise-specific Features
|
||
|
|
|
||
|
|
### Audit Logging
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Service
|
||
|
|
@ConditionalOnProperty(prefix = "appsmith", name = "audit.enabled", havingValue = "true")
|
||
|
|
public class AuditServiceImpl implements AuditService {
|
||
|
|
private final AuditRepository repository;
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public Mono<AuditLog> log(String action, String resourceId, String resourceType, User user) {
|
||
|
|
AuditLog log = new AuditLog();
|
||
|
|
log.setAction(action);
|
||
|
|
log.setResourceId(resourceId);
|
||
|
|
log.setResourceType(resourceType);
|
||
|
|
log.setUserId(user.getId());
|
||
|
|
log.setUsername(user.getUsername());
|
||
|
|
log.setTimestamp(Instant.now());
|
||
|
|
|
||
|
|
return repository.save(log);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Role-Based Access Control
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Service
|
||
|
|
public class PermissionServiceImpl implements PermissionService {
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public Mono<Boolean> hasPermission(User user, String resourceId, PermissionType permission) {
|
||
|
|
return userGroupRepository.findByUserIdAndOrganizationId(user.getId(), user.getCurrentOrganizationId())
|
||
|
|
.flatMap(userGroup -> {
|
||
|
|
if (userGroup.getRole() == UserRole.ORGANIZATION_ADMIN) {
|
||
|
|
return Mono.just(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
return resourcePermissionRepository
|
||
|
|
.findByResourceIdAndPermission(resourceId, permission)
|
||
|
|
.any(resourcePermission -> resourcePermission.getUserGroupId().equals(userGroup.getId()));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### SSO Integration
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Configuration
|
||
|
|
@ConditionalOnProperty(prefix = "appsmith.oauth2", name = "enabled", havingValue = "true")
|
||
|
|
public class OAuth2Config {
|
||
|
|
|
||
|
|
@Bean
|
||
|
|
public ReactiveClientRegistrationRepository clientRegistrationRepository() {
|
||
|
|
List<ClientRegistration> registrations = new ArrayList<>();
|
||
|
|
|
||
|
|
registrations.add(googleClientRegistration());
|
||
|
|
registrations.add(githubClientRegistration());
|
||
|
|
// Other providers...
|
||
|
|
|
||
|
|
return new InMemoryReactiveClientRegistrationRepository(registrations);
|
||
|
|
}
|
||
|
|
|
||
|
|
private ClientRegistration googleClientRegistration() {
|
||
|
|
return ClientRegistration.withRegistrationId("google")
|
||
|
|
.clientId(googleClientId)
|
||
|
|
.clientSecret(googleClientSecret)
|
||
|
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||
|
|
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||
|
|
.redirectUri("{baseUrl}/api/v1/oauth2/callback/{registrationId}")
|
||
|
|
.scope("openid", "email", "profile")
|
||
|
|
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
|
||
|
|
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
|
||
|
|
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
|
||
|
|
.userNameAttributeName(IdTokenClaimNames.SUB)
|
||
|
|
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
|
||
|
|
.clientName("Google")
|
||
|
|
.build();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Performance Optimization Techniques
|
||
|
|
|
||
|
|
### Frontend Optimizations
|
||
|
|
|
||
|
|
1. **Memoization**:
|
||
|
|
```typescript
|
||
|
|
const MemoizedComponent = React.memo(MyComponent);
|
||
|
|
|
||
|
|
// Using useMemo for computed values
|
||
|
|
const computedValue = useMemo(() => {
|
||
|
|
return expensiveComputation(a, b);
|
||
|
|
}, [a, b]);
|
||
|
|
|
||
|
|
// Using useCallback for functions
|
||
|
|
const handleClick = useCallback(() => {
|
||
|
|
doSomething(a, b);
|
||
|
|
}, [a, b]);
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Virtualization for Large Lists**:
|
||
|
|
```typescript
|
||
|
|
import { FixedSizeList } from 'react-window';
|
||
|
|
|
||
|
|
const MyList = ({ items }) => (
|
||
|
|
<FixedSizeList
|
||
|
|
height={500}
|
||
|
|
width={500}
|
||
|
|
itemCount={items.length}
|
||
|
|
itemSize={50}
|
||
|
|
>
|
||
|
|
{({ index, style }) => (
|
||
|
|
<div style={style}>
|
||
|
|
{items[index].name}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</FixedSizeList>
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Backend Optimizations
|
||
|
|
|
||
|
|
1. **Batch Processing**:
|
||
|
|
```java
|
||
|
|
@Service
|
||
|
|
public class BatchImportServiceImpl implements BatchImportService {
|
||
|
|
@Override
|
||
|
|
public Mono<ImportResult> importEntities(List<Entity> entities) {
|
||
|
|
return Flux.fromIterable(entities)
|
||
|
|
.flatMap(this::validateEntity)
|
||
|
|
.collectList()
|
||
|
|
.flatMap(validatedEntities ->
|
||
|
|
repository.saveAll(validatedEntities)
|
||
|
|
.collectList()
|
||
|
|
.map(savedEntities -> new ImportResult(savedEntities.size(), null))
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Query Optimization**:
|
||
|
|
```java
|
||
|
|
// Using MongoDB indexes
|
||
|
|
@Document
|
||
|
|
public class Application {
|
||
|
|
// ...
|
||
|
|
|
||
|
|
@Indexed
|
||
|
|
private String name;
|
||
|
|
|
||
|
|
@Indexed
|
||
|
|
private String organizationId;
|
||
|
|
|
||
|
|
@CompoundIndex(def = "{'organizationId': 1, 'name': 1}", unique = true)
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## This document should provide Cursor with a deeper understanding of the technical implementation details of Appsmith, allowing for more accurate and contextual assistance when working with the codebase.
|