npm package: @lokalise/connector-shell
Description: Connector shell module with controllers for Lokalise Content Engine connectors
Author: Lokalise
Latest version: 3.5.0

# @lokalise/connector-shell
    
A drop-in module for [opinionated-machine](https://github.com/lokalise/opinionated-machine) services that provides the connector shell infrastructure for Lokalise Content Engine connectors.

This package provides the generic connector shell with all standard controllers (Auth, Cache, Env, ItemList, ItemGroup, Publish, Translate) and routing logic. It's designed to work with any set of adapters through automatic adapter discovery via dependency injection tags.

## Features

- **Drop-in Integration**: Add to your modules list alongside adapter modules
- **Automatic Adapter Discovery**: Finds adapters via DI tags (`ADAPTER_LABEL = 'connectorAdapter'`)
- **Complete Controller Suite**: All 7 standard controllers included
- **Type-Safe**: Full TypeScript support

## Installation

```bash
npm install @lokalise/connector-shell
```

## Quick Start

```typescript
// modules.ts
export const ALL_MODULES = [
  new CommonModule(),          // Provides required dependencies
  new YourAdapterModule(),     // Registers adapters with ADAPTER_LABEL tag
  new ConnectorShellModule(),  // Discovers adapters, registers controllers
]
```

## Required Dependencies

Your `CommonModule` must provide:

```typescript
import { ADAPTER_LABEL } from '@lokalise/connector-adapter-common'
import type { ProtectedRouteMetadataMapperFactory } from '@lokalise/connector-shell'

export class CommonModule extends AbstractModule<CommonDependencies> {
  resolveDependencies() {
    return {
      awilixManager: asSingletonFunction((deps) => deps.app?.awilixManager),
      protectedRouteMetadataMapperFactory: asSingletonFunction(
        (): ProtectedRouteMetadataMapperFactory => PROTECTED_ROUTE_METADATA_MAPPER
      ),
    }
  }
}

export interface CommonDependencies {
  awilixManager: AwilixManager
  protectedRouteMetadataMapperFactory: ProtectedRouteMetadataMapperFactory
}
```

**Example prehandler:**

```typescript
// prehandlers/integrationConfigPrehandler.ts
export const PROTECTED_ROUTE_METADATA_MAPPER = (requireAuthConfig = true) =>
  (() => ({
    preHandler: (req, res, done) => {
      // Decode ce-config and ce-auth headers, attach to req
      done()
    }
  })) satisfies ApiContractMetadataToRouteMapper
```

## Adapter Discovery

Adapters are discovered via the `ADAPTER_LABEL` tag:

```typescript
import { ADAPTER_LABEL, type Adapter } from '@lokalise/connector-adapter-common'
import { AbstractModule, asSingletonClass } from 'opinionated-machine'

export class YourAdapterModule extends AbstractModule<YourDependencies> {
  resolveDependencies() {
    return {
      yourApiClient: asSingletonClass(YourApiClient),
      yourAdapter: asSingletonClass(YourAdapter, {
        tags: [ADAPTER_LABEL], // <-- Required for discovery
      }),
    }
  }
}
```

**How it works:**
- `ADAPTER_LABEL = 'connectorAdapter'` (constant from `@lokalise/connector-adapter-common`)
- ConnectorShellModule queries `awilixManager.getWithTags([ADAPTER_LABEL])` to find all adapters
- Adapters are keyed by `adapter.getConnectorName()` value
- Controllers resolve adapters per-request via `ce-connector-id` header

## Creating Your ConnectorShellModule

```typescript
import {
  ConnectorShellModule as BaseConnectorShellModule,
  type ConnectorShellBaseDependencies,
} from '@lokalise/connector-shell'

export type SupportedConnectors =
  | typeof YourAdapter.connectorName
  | 'another-connector'

export type ConnectorShellInjectableDependencies =
  ConnectorShellBaseDependencies &
  CommonDependencies &
  YourAdapterPublicDependencies

export class ConnectorShellModule extends BaseConnectorShellModule<
  ConnectorShellInjectableDependencies,
  ExternalDependencies
> {}
```

## Bootstrap

```typescript
import { DIContext } from 'opinionated-machine'
import { ALL_MODULES } from './modules'

const diContext = new DIContext(diContainer, options)
diContext.registerDependencies({ modules: ALL_MODULES }, externalDependencies)

app.after(() => {
  diContext.registerRoutes(app) // Controllers auto-register
})
```

## Adding a New Adapter

1. Create adapter implementing `Adapter` interface
2. Register with `ADAPTER_LABEL` tag:
   ```typescript
   yourAdapter: asSingletonClass(YourAdapter, {
     tags: [ADAPTER_LABEL],
   })
   ```
.