Skip to content

Commit a18818f

Browse files
authored
fix: use users-permissions as non-plugin collections (#104)
1 parent f477289 commit a18818f

File tree

7 files changed

+140
-16
lines changed

7 files changed

+140
-16
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,33 @@ const updatedArticle = await articles.update('article-document-id', { title: 'Up
207207
await articles.delete('article-id');
208208
```
209209

210+
#### Working with Users (Users-Permissions Plugin)
211+
212+
The client automatically detects and handles special Strapi content-types like `users` from the users-permissions plugin. You can work with them seamlessly:
213+
214+
```typescript
215+
// Auto-detected as users-permissions - no configuration needed!
216+
const users = client.collection('users');
217+
218+
// Create a new user
219+
await users.create({
220+
username: 'john',
221+
222+
password: 'SecurePass123!',
223+
role: 1,
224+
});
225+
226+
// Find users
227+
const allUsers = await users.find();
228+
229+
// Update a user
230+
await users.update('user-id', { username: 'john_updated' });
231+
```
232+
233+
> **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.
234+
235+
#### Custom Path
236+
210237
You can also customize the root path for requests by providing a value for the `path` option:
211238

212239
```typescript

demo/node-typescript/src/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,8 @@ async function demonstrateUserCreation() {
213213
console.log(os.EOL);
214214

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

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

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

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

268269
console.log('User docs:');

src/client.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import createDebug from 'debug';
22

33
import { AuthManager } from './auth';
44
import { CollectionTypeManager, SingleTypeManager } from './content-types';
5+
import { WELL_KNOWN_STRAPI_RESOURCES } from './content-types/constants';
56
import { StrapiError, StrapiInitializationError } from './errors';
67
import { FilesManager } from './files';
78
import { HttpClient } from './http';
@@ -341,14 +342,9 @@ export class StrapiClient {
341342
* const customArticles = client.collection('articles', { path: '/custom-articles-path' });
342343
* const customAllArticles = await customArticles.find();
343344
*
344-
* // Example: Working with users-permissions plugin (no data wrapping, no route prefix)
345-
* const users = client.collection('users', {
346-
* plugin: {
347-
* name: 'users-permissions',
348-
* prefix: '' // some users-permissions routes are not prefixed
349-
* }
350-
* });
351-
*
345+
* // Example: Working with users (auto-detected as users-permissions plugin)
346+
* const users = client.collection('users');
347+
* await users.create({ username: 'john', email: 'john@example.com', password: 'Test1234!' });
352348
*
353349
* // Example: Working with a custom plugin (routes prefixed by default)
354350
* const posts = client.collection('posts', {
@@ -367,7 +363,11 @@ export class StrapiClient {
367363
collection(resource: string, options: ClientCollectionOptions = {}) {
368364
const { path, plugin } = options;
369365

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

373373
/**

src/content-types/collection/manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import createDebug from 'debug';
33
import { HttpClient } from '../../http';
44
import { URLHelper } from '../../utilities';
55
import { AbstractContentTypeManager } from '../abstract';
6+
import { pluginsThatDoNotWrapDataAttribute } from '../constants';
67

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

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

12-
const pluginsThatDoNotWrapDataAttribute = ['users-permissions'];
1313
/**
1414
* A service class designed for interacting with a collection-type resource in a Strapi app.
1515
*

src/content-types/constants.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Mapping of well-known Strapi resource names to their plugin configurations.
3+
* This enables automatic handling of special Strapi content-types that have
4+
* different API contracts than regular content-types.
5+
*/
6+
export const WELL_KNOWN_STRAPI_RESOURCES: Record<
7+
string,
8+
{ plugin: { name: string; prefix: string } }
9+
> = {
10+
// Users from users-permissions plugin don't wrap data and have no route prefix
11+
users: {
12+
plugin: {
13+
name: 'users-permissions',
14+
prefix: '',
15+
},
16+
},
17+
};
18+
19+
/**
20+
* List of plugin names that do not wrap the inner payload in a "data" attribute.
21+
*/
22+
export const pluginsThatDoNotWrapDataAttribute = ['users-permissions'];

tests/unit/client.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,62 @@ describe('Strapi', () => {
194194
expect(collection).toBeInstanceOf(CollectionTypeManager);
195195
expect(collection).toHaveProperty('_options', { resource });
196196
});
197+
198+
it('should auto-detect "users" resource and apply users-permissions plugin configuration', () => {
199+
// Arrange
200+
const resource = 'users';
201+
const config = { baseURL: 'https://localhost:1337/api' } satisfies StrapiClientConfig;
202+
203+
const mockValidator = new MockStrapiConfigValidator();
204+
const mockAuthManager = new MockAuthManager();
205+
206+
const client = new StrapiClient(
207+
config,
208+
mockValidator,
209+
mockAuthManager,
210+
mockHttpClientFactory
211+
);
212+
213+
// Act
214+
const collection = client.collection(resource);
215+
216+
// Assert
217+
expect(collection).toBeInstanceOf(CollectionTypeManager);
218+
expect(collection).toHaveProperty('_options', {
219+
resource: 'users',
220+
plugin: {
221+
name: 'users-permissions',
222+
prefix: '',
223+
},
224+
});
225+
});
226+
227+
it('should allow explicit plugin option to override auto-detection', () => {
228+
// Arrange
229+
const resource = 'users';
230+
const customPlugin = { name: 'custom-plugin', prefix: 'custom' };
231+
const config = { baseURL: 'https://localhost:1337/api' } satisfies StrapiClientConfig;
232+
233+
const mockValidator = new MockStrapiConfigValidator();
234+
const mockAuthManager = new MockAuthManager();
235+
236+
const client = new StrapiClient(
237+
config,
238+
mockValidator,
239+
mockAuthManager,
240+
mockHttpClientFactory
241+
);
242+
243+
// Act
244+
const collection = client.collection(resource, { plugin: customPlugin });
245+
246+
// Assert
247+
expect(collection).toBeInstanceOf(CollectionTypeManager);
248+
expect(collection).toHaveProperty('_options', {
249+
resource: 'users',
250+
plugin: customPlugin,
251+
});
252+
});
197253
});
198254

199255
describe('Single', () => {

tests/unit/content-types/collection/collection-manager.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,24 @@ describe('CollectionTypeManager CRUD Methods', () => {
271271
},
272272
});
273273
});
274+
275+
it('should wrap data for regular content-types that are not users-permissions', async () => {
276+
// Arrange
277+
const articlesManager = new CollectionTypeManager({ resource: 'articles' }, mockHttpClient);
278+
const payload = { title: 'Test Article', content: 'Test content' };
279+
280+
// Act
281+
await articlesManager.create(payload);
282+
283+
// Assert - Should wrap payload in data object
284+
expect(mockHttpClient.request).toHaveBeenCalledWith('/articles', {
285+
method: 'POST',
286+
body: JSON.stringify({ data: payload }),
287+
headers: {
288+
'Content-Type': 'application/json',
289+
},
290+
});
291+
});
274292
});
275293

276294
describe('Plugin Route Prefixing', () => {

0 commit comments

Comments
 (0)