Skip to content
Merged
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
32 changes: 15 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,10 @@ If your Strapi instance uses API tokens, configure the SDK like this:

```typescript
const sdk = strapi({
// Endpoint configuration
baseURL: 'http://localhost:1337/api',
auth: {
strategy: 'api-token',
options: { token: 'your-api-token-here' },
},
// Auth configuration
auth: 'your-api-token-here',
});
```

Expand Down Expand Up @@ -239,16 +238,15 @@ The `debug` tool allows you to control logs using wildcard patterns (`*`):

Below is a list of available namespaces to use:

| Namespace | Description |
| ---------------------------------------- | ----------------------------------------------------------------------------------------- |
| `strapi:core` | Logs SDK initialization, configuration validation, and HTTP client setup. |
| `strapi:validators:config` | Logs details related to SDK configuration validation. |
| `strapi:validators:url` | Logs URL validation processes. |
| `strapi:http` | Logs HTTP client setup, request processing, and response/error handling. |
| `strapi:auth:factory` | Logs the registration and creation of authentication providers. |
| `strapi:auth:manager` | Logs authentication lifecycle management. |
| `strapi:auth:provider:api-token` | Logs operations related to API token authentication. |
| `strapi:auth:provider:users-permissions` | Logs operations related to user and permissions-based authentication. |
| `strapi:ct:collection` | Logs interactions with collection-type content managers. |
| `strapi:ct:single` | Logs interactions with single-type content managers. |
| `strapi:utils:url-helper` | Logs URL helper utility operations (e.g., appending query parameters or formatting URLs). |
| Namespace | Description |
| -------------------------------- | ----------------------------------------------------------------------------------------- |
| `strapi:core` | Logs SDK initialization, configuration validation, and HTTP client setup. |
| `strapi:validators:config` | Logs details related to SDK configuration validation. |
| `strapi:validators:url` | Logs URL validation processes. |
| `strapi:http` | Logs HTTP client setup, request processing, and response/error handling. |
| `strapi:auth:factory` | Logs the registration and creation of authentication providers. |
| `strapi:auth:manager` | Logs authentication lifecycle management. |
| `strapi:auth:provider:api-token` | Logs operations related to API token authentication. |
| `strapi:ct:collection` | Logs interactions with collection-type content managers. |
| `strapi:ct:single` | Logs interactions with single-type content managers. |
| `strapi:utils:url-helper` | Logs URL helper utility operations (e.g., appending query parameters or formatting URLs). |
68 changes: 54 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,46 @@
import { ApiTokenAuthProvider } from './auth';
import { Strapi } from './sdk';
import { StrapiConfigValidator } from './validators';

import type { StrapiConfig } from './sdk';

export * from './errors';

export interface Config {
/**
* The base URL of the Strapi content API.
*
* This specifies where the SDK should send requests.
*
* The URL must include the protocol (`http` or `https`) and serve
* as the root path for all later API operations.
*
* @example
* 'https://api.example.com'
*
* @remarks
* Failing to provide a valid HTTP or HTTPS URL results in a
* `StrapiInitializationError`.
*/
baseURL: string;

/**
* API token to authenticate requests (optional).
*
* When provided, this token is included in the `Authorization` header
* of every request to the Strapi API.
*
* @remarks
* - A valid token must be a non-empty string.
*
* - If the token is invalid or improperly formatted, the SDK
* throws a `StrapiValidationError` during initialization.
*
* - If excluded, the SDK operates without authentication.
*/

auth?: string;
}

/**
* Creates a new instance of the Strapi SDK with a specified configuration.
*
Expand All @@ -25,10 +61,7 @@ export * from './errors';
* // Basic configuration using API token auth
* const config = {
* baseURL: 'https://api.example.com',
* auth: {
* strategy: 'api-token',
* options: { token: 'your_token_here' }
* }
* auth: 'your_token_here',
* };
*
* // Create the SDK instance
Expand All @@ -44,13 +77,20 @@ export * from './errors';
* @throws {StrapiInitializationError} If the provided baseURL doesn't conform to a valid HTTP or HTTPS URL,
* or if the auth configuration is invalid.
*/
export const strapi = (config: StrapiConfig) => {
const configValidator = new StrapiConfigValidator();

return new Strapi<typeof config>(
// Properties
config,
// Dependencies
configValidator
);
export const strapi = (config: Config) => {
const { baseURL, auth } = config;

const sdkConfig: StrapiConfig = { baseURL };

// In this factory, while there is only one auth strategy available, users can't manually set the strategy options.
// Since the SDK constructor needs to define a proper strategy,
// it is handled here if the auth property is provided
if (auth !== undefined) {
sdkConfig.auth = {
strategy: ApiTokenAuthProvider.identifier,
options: { token: auth },
};
}

return new Strapi(sdkConfig);
};
19 changes: 12 additions & 7 deletions src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import createDebug from 'debug';

import { AuthManager } from './auth';
import { CollectionTypeManager, SingleTypeManager } from './content-types';
import { StrapiInitializationError } from './errors';
import { StrapiError, StrapiInitializationError } from './errors';
import { HttpClient } from './http';
import { AuthInterceptors, HttpInterceptors } from './interceptors';
import { StrapiConfigValidator } from './validators';
Expand Down Expand Up @@ -39,12 +39,10 @@ export interface AuthConfig<T = unknown> {
*
* It serves as the main interface through which users interact with
* their Strapi installation programmatically.
*
* @template T_Config - Configuration type inferred from the user-provided SDK configuration
*/
export class Strapi<const T_Config extends StrapiConfig = StrapiConfig> {
export class Strapi {
/** @internal */
private readonly _config: T_Config;
private readonly _config: StrapiConfig;

/** @internal */
private readonly _validator: StrapiConfigValidator;
Expand All @@ -58,7 +56,7 @@ export class Strapi<const T_Config extends StrapiConfig = StrapiConfig> {
/** @internal */
constructor(
// Properties
config: T_Config,
config: StrapiConfig,

// Dependencies
validator: StrapiConfigValidator = new StrapiConfigValidator(),
Expand Down Expand Up @@ -189,7 +187,14 @@ export class Strapi<const T_Config extends StrapiConfig = StrapiConfig> {

debug('setting up the auth strategy using %o', strategy);

this._authManager.setStrategy(strategy, options);
try {
this._authManager.setStrategy(strategy, options);
} catch (e) {
throw new StrapiInitializationError(
e,
`Failed to initialize the SDK auth manager: ${e instanceof StrapiError ? e.cause : e}`
);
}
}

this._httpClient.interceptors.request
Expand Down
36 changes: 25 additions & 11 deletions tests/unit/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { strapi, StrapiInitializationError, StrapiValidationError } from '../../src';
import { strapi, StrapiInitializationError } from '../../src';
import { ApiTokenAuthProvider } from '../../src/auth';
import { Strapi } from '../../src/sdk';

import type { StrapiConfig } from '../../src/sdk';
import type { Config } from '../../src';

describe('strapi', () => {
it('should create an SDK instance with valid configuration', () => {
it('should create an SDK instance with valid http configuration', () => {
// Arrange
const config = { baseURL: 'https://api.example.com' } satisfies StrapiConfig;
const config = { baseURL: 'https://api.example.com' } satisfies Config;

// Act
const sdk = strapi(config);
Expand All @@ -16,9 +17,25 @@ describe('strapi', () => {
expect(sdk).toHaveProperty('baseURL', config.baseURL);
});

it('should create an SDK instance with valid auth configuration', () => {
// Arrange
const token = '<token>';
const config = { baseURL: 'https://api.example.com', auth: token } satisfies Config;

// Act
const sdk = strapi(config);

// Assert
expect(sdk).toBeInstanceOf(Strapi);
expect(sdk).toHaveProperty('auth', {
strategy: ApiTokenAuthProvider.identifier, // default auth strategy
options: { token },
});
});

it('should throw an error for an invalid baseURL', () => {
// Arrange
const config = { baseURL: 'invalid-url' } satisfies StrapiConfig;
const config = { baseURL: 'invalid-url' } satisfies Config;

// Act & Assert
expect(() => strapi(config)).toThrow(StrapiInitializationError);
Expand All @@ -28,13 +45,10 @@ describe('strapi', () => {
// Arrange
const config = {
baseURL: 'https://api.example.com',
auth: {
strategy: 'api-token',
options: { token: '' }, // Invalid token
},
} satisfies StrapiConfig;
auth: '', // Invalid API token
} satisfies Config;

// Act & Assert
expect(() => strapi(config)).toThrow(StrapiValidationError);
expect(() => strapi(config)).toThrow(StrapiInitializationError);
});
});
37 changes: 35 additions & 2 deletions tests/unit/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
HTTPInternalServerError,
HTTPNotFoundError,
HTTPTimeoutError,
StrapiError,
StrapiInitializationError,
StrapiValidationError,
} from '../../src';
import { CollectionTypeManager, SingleTypeManager } from '../../src/content-types';
import { HttpClient, StatusCode } from '../../src/http';
Expand Down Expand Up @@ -46,7 +48,7 @@ describe('Strapi', () => {
// Arrange
const config = {
baseURL: 'https://localhost:1337/api',
auth: { strategy: MockAuthProvider.identifier, options: {} },
auth: { strategy: MockAuthProvider.identifier },
} satisfies StrapiConfig;

const mockValidator = new MockStrapiConfigValidator();
Expand All @@ -62,7 +64,7 @@ describe('Strapi', () => {

expect(sdk).toBeInstanceOf(Strapi);
expect(validatorSpy).toHaveBeenCalledWith(config);
expect(authSetStrategySpy).toHaveBeenCalledWith(MockAuthProvider.identifier, {});
expect(authSetStrategySpy).toHaveBeenCalledWith(MockAuthProvider.identifier, undefined);
});

it('should not set the auth strategy if no auth config is provided', () => {
Expand All @@ -82,6 +84,37 @@ describe('Strapi', () => {
expect(authSetStrategySpy).not.toHaveBeenCalled();
});

it.each([
['common error', new Error('unexpected error')],
['strapi error', new StrapiError(new StrapiValidationError('invalid auth configuration'))],
])(
'should throw an initialization error if a %s error occurs during the auth strategy init',
async (_title, error) => {
// Arrange
const config = {
baseURL: 'https://localhost:1337/api',
auth: { strategy: MockAuthProvider.identifier },
} satisfies StrapiConfig;

const mockValidator = new MockStrapiConfigValidator();
const mockAuthManager = new MockAuthManager();

jest.spyOn(mockAuthManager, 'setStrategy').mockImplementationOnce(() => {
throw error;
});

// Act
expect(
() => new Strapi(config, mockValidator, mockAuthManager, mockHttpClientFactory)
).toThrow(StrapiInitializationError);

expect(mockAuthManager.setStrategy).toHaveBeenCalledWith(
MockAuthProvider.identifier,
undefined
);
}
);

it('should throw an error on invalid baseURL', () => {
// Arrange
const config = { baseURL: 'invalid-url' } satisfies StrapiConfig;
Expand Down