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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,33 @@ const updatedArticle = await articles.update('article-document-id', { title: 'Up
await articles.delete('article-id');
```

#### Working with Users (Users-Permissions Plugin)

The client automatically detects and handles special Strapi content-types like `users` from the users-permissions plugin. You can work with them seamlessly:

```typescript
// Auto-detected as users-permissions - no configuration needed!
const users = client.collection('users');

// Create a new user
await users.create({
username: 'john',
email: 'john@example.com',
password: 'SecurePass123!',
role: 1,
});

// Find users
const allUsers = await users.find();

// Update a user
await users.update('user-id', { username: 'john_updated' });
```

> **Note:** The users-permissions plugin has a different API contract than regular content-types. The client automatically handles this by not wrapping the payload in a `data` object.

#### Custom Path

You can also customize the root path for requests by providing a value for the `path` option:

```typescript
Expand Down
13 changes: 7 additions & 6 deletions demo/node-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,8 @@ async function demonstrateUserCreation() {
console.log(os.EOL);

try {
// Use plugin-specific configuration for users-permissions
const users = client.collection('users', {
plugin: { name: 'users-permissions', prefix: '' },
});
// Auto-detected as users-permissions - no plugin configuration needed!
const users = client.collection('users');

// Generate unique user data for the demo
const timestamp = Date.now();
Expand All @@ -227,7 +225,7 @@ async function demonstrateUserCreation() {
role: 1,
};

console.log('Creating new user with users-permissions plugin configuration...');
console.log('Creating new user (auto-detected as users-permissions)...');
console.log(` Username: ${newUserData.username}`);
console.log(` Email: ${newUserData.email}`);
console.log(` Role: ${newUserData.role}`);
Expand Down Expand Up @@ -262,7 +260,10 @@ async function demonstrateUserUpdate() {

try {
// Get all users to find one to update
const users = client.collection('users', { plugin: { name: 'users-permissions', prefix: '' } });
const users = client.collection(
// Auto-detected as users-permissions - no plugin configuration needed!
'users'
);
const userDocs = (await users.find()) as unknown as UsersArray;

console.log('User docs:');
Expand Down
18 changes: 9 additions & 9 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import createDebug from 'debug';

import { AuthManager } from './auth';
import { CollectionTypeManager, SingleTypeManager } from './content-types';
import { WELL_KNOWN_STRAPI_RESOURCES } from './content-types/constants';
import { StrapiError, StrapiInitializationError } from './errors';
import { FilesManager } from './files';
import { HttpClient } from './http';
Expand Down Expand Up @@ -341,14 +342,9 @@ export class StrapiClient {
* const customArticles = client.collection('articles', { path: '/custom-articles-path' });
* const customAllArticles = await customArticles.find();
*
* // Example: Working with users-permissions plugin (no data wrapping, no route prefix)
* const users = client.collection('users', {
* plugin: {
* name: 'users-permissions',
* prefix: '' // some users-permissions routes are not prefixed
* }
* });
*
* // Example: Working with users (auto-detected as users-permissions plugin)
* const users = client.collection('users');
* await users.create({ username: 'john', email: 'john@example.com', password: 'Test1234!' });
*
* // Example: Working with a custom plugin (routes prefixed by default)
* const posts = client.collection('posts', {
Expand All @@ -367,7 +363,11 @@ export class StrapiClient {
collection(resource: string, options: ClientCollectionOptions = {}) {
const { path, plugin } = options;

return new CollectionTypeManager({ resource, path, plugin }, this._httpClient);
// Auto-detect well-known Strapi resources and apply their plugin configuration
// if no explicit plugin option is provided
const effectivePlugin = plugin ?? WELL_KNOWN_STRAPI_RESOURCES[resource]?.plugin ?? undefined;

return new CollectionTypeManager({ resource, path, plugin: effectivePlugin }, this._httpClient);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/content-types/collection/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import createDebug from 'debug';
import { HttpClient } from '../../http';
import { URLHelper } from '../../utilities';
import { AbstractContentTypeManager } from '../abstract';
import { pluginsThatDoNotWrapDataAttribute } from '../constants';

import type * as API from '../../types/content-api';
import type { ContentTypeManagerOptions } from '../abstract';

const debug = createDebug('strapi:ct:collection');

const pluginsThatDoNotWrapDataAttribute = ['users-permissions'];
/**
* A service class designed for interacting with a collection-type resource in a Strapi app.
*
Expand Down
22 changes: 22 additions & 0 deletions src/content-types/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Mapping of well-known Strapi resource names to their plugin configurations.
* This enables automatic handling of special Strapi content-types that have
* different API contracts than regular content-types.
*/
export const WELL_KNOWN_STRAPI_RESOURCES: Record<
string,
{ plugin: { name: string; prefix: string } }
> = {
// Users from users-permissions plugin don't wrap data and have no route prefix
users: {
plugin: {
name: 'users-permissions',
prefix: '',
},
},
};

/**
* List of plugin names that do not wrap the inner payload in a "data" attribute.
*/
export const pluginsThatDoNotWrapDataAttribute = ['users-permissions'];
56 changes: 56 additions & 0 deletions tests/unit/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,62 @@ describe('Strapi', () => {
expect(collection).toBeInstanceOf(CollectionTypeManager);
expect(collection).toHaveProperty('_options', { resource });
});

it('should auto-detect "users" resource and apply users-permissions plugin configuration', () => {
// Arrange
const resource = 'users';
const config = { baseURL: 'https://localhost:1337/api' } satisfies StrapiClientConfig;

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

const client = new StrapiClient(
config,
mockValidator,
mockAuthManager,
mockHttpClientFactory
);

// Act
const collection = client.collection(resource);

// Assert
expect(collection).toBeInstanceOf(CollectionTypeManager);
expect(collection).toHaveProperty('_options', {
resource: 'users',
plugin: {
name: 'users-permissions',
prefix: '',
},
});
});

it('should allow explicit plugin option to override auto-detection', () => {
// Arrange
const resource = 'users';
const customPlugin = { name: 'custom-plugin', prefix: 'custom' };
const config = { baseURL: 'https://localhost:1337/api' } satisfies StrapiClientConfig;

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

const client = new StrapiClient(
config,
mockValidator,
mockAuthManager,
mockHttpClientFactory
);

// Act
const collection = client.collection(resource, { plugin: customPlugin });

// Assert
expect(collection).toBeInstanceOf(CollectionTypeManager);
expect(collection).toHaveProperty('_options', {
resource: 'users',
plugin: customPlugin,
});
});
});

describe('Single', () => {
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/content-types/collection/collection-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,24 @@ describe('CollectionTypeManager CRUD Methods', () => {
},
});
});

it('should wrap data for regular content-types that are not users-permissions', async () => {
// Arrange
const articlesManager = new CollectionTypeManager({ resource: 'articles' }, mockHttpClient);
const payload = { title: 'Test Article', content: 'Test content' };

// Act
await articlesManager.create(payload);

// Assert - Should wrap payload in data object
expect(mockHttpClient.request).toHaveBeenCalledWith('/articles', {
method: 'POST',
body: JSON.stringify({ data: payload }),
headers: {
'Content-Type': 'application/json',
},
});
});
});

describe('Plugin Route Prefixing', () => {
Expand Down