From 4c0613b05d2d1079c21a2dce78e47dd2c8561061 Mon Sep 17 00:00:00 2001 From: nclsndr Date: Fri, 24 Oct 2025 15:39:56 +0200 Subject: [PATCH 1/2] fix: users-permissions case --- README.md | 27 +++++++++ demo/node-typescript/src/index.ts | 13 +++-- src/client.ts | 32 ++++++++--- tests/unit/client.test.ts | 56 +++++++++++++++++++ .../collection/collection-manager.test.ts | 18 ++++++ 5 files changed, 131 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 72e1b31..3e57ea5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/demo/node-typescript/src/index.ts b/demo/node-typescript/src/index.ts index 60d7f0a..407b307 100644 --- a/demo/node-typescript/src/index.ts +++ b/demo/node-typescript/src/index.ts @@ -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(); @@ -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}`); @@ -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:'); diff --git a/src/client.ts b/src/client.ts index 75d8a3f..2e4bd06 100644 --- a/src/client.ts +++ b/src/client.ts @@ -13,6 +13,21 @@ import type { HttpClientConfig } from './http'; const debug = createDebug('strapi:core'); +/** + * 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. + */ +const WELL_KNOWN_STRAPI_RESOURCES: Record = { + // Users from users-permissions plugin don't wrap data and have no route prefix + users: { + plugin: { + name: 'users-permissions', + prefix: '', + }, + }, +}; + export interface StrapiClientConfig { /** The base URL of the Strapi content API, required for all client library operations. */ baseURL: string; @@ -341,14 +356,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', { @@ -367,7 +377,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); } /** diff --git a/tests/unit/client.test.ts b/tests/unit/client.test.ts index 92a2db8..2be1285 100644 --- a/tests/unit/client.test.ts +++ b/tests/unit/client.test.ts @@ -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', () => { diff --git a/tests/unit/content-types/collection/collection-manager.test.ts b/tests/unit/content-types/collection/collection-manager.test.ts index 84f3ed2..f09152f 100644 --- a/tests/unit/content-types/collection/collection-manager.test.ts +++ b/tests/unit/content-types/collection/collection-manager.test.ts @@ -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', () => { From 6c798c5f5775ed1a766c61f629b27767ee9a23ee Mon Sep 17 00:00:00 2001 From: nclsndr Date: Fri, 24 Oct 2025 17:09:27 +0200 Subject: [PATCH 2/2] enhancement: constants --- src/client.ts | 16 +--------------- src/content-types/collection/manager.ts | 2 +- src/content-types/constants.ts | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 src/content-types/constants.ts diff --git a/src/client.ts b/src/client.ts index 2e4bd06..5339db5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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'; @@ -13,21 +14,6 @@ import type { HttpClientConfig } from './http'; const debug = createDebug('strapi:core'); -/** - * 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. - */ -const WELL_KNOWN_STRAPI_RESOURCES: Record = { - // Users from users-permissions plugin don't wrap data and have no route prefix - users: { - plugin: { - name: 'users-permissions', - prefix: '', - }, - }, -}; - export interface StrapiClientConfig { /** The base URL of the Strapi content API, required for all client library operations. */ baseURL: string; diff --git a/src/content-types/collection/manager.ts b/src/content-types/collection/manager.ts index dfd2ce2..aa38384 100644 --- a/src/content-types/collection/manager.ts +++ b/src/content-types/collection/manager.ts @@ -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. * diff --git a/src/content-types/constants.ts b/src/content-types/constants.ts new file mode 100644 index 0000000..1b9389d --- /dev/null +++ b/src/content-types/constants.ts @@ -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'];