-
Notifications
You must be signed in to change notification settings - Fork 1
feat(adt): add user lookup command and system contract #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
431e4e1
59dd89c
f1d980d
5554994
0dbaa68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,136 @@ | ||||||
| import { Command } from 'commander'; | ||||||
| import { getAdtClientV2 } from '../utils/adt-client-v2'; | ||||||
|
|
||||||
| /** Extract entries from atomFeed parsed response */ | ||||||
| function extractEntries( | ||||||
| data: unknown, | ||||||
| ): { id?: string; title?: string; link?: { href: string }[] }[] { | ||||||
| const feed = data as Record<string, unknown>; | ||||||
| const feedData = feed.feed as Record<string, unknown> | undefined; | ||||||
| if (!feedData) return []; | ||||||
|
|
||||||
| const rawEntries = feedData.entry; | ||||||
| if (!rawEntries) return []; | ||||||
| return Array.isArray(rawEntries) ? rawEntries : [rawEntries]; | ||||||
| } | ||||||
|
|
||||||
| export const userCommand = new Command('user') | ||||||
| .description('Look up SAP system users') | ||||||
| .argument('[query]', 'Username or search query (supports wildcards like *)') | ||||||
| .option('-m, --max <number>', 'Maximum number of results', '50') | ||||||
| .option('--json', 'Output results as JSON') | ||||||
| .action(async (query: string | undefined, options) => { | ||||||
|
Check failure on line 22 in packages/adt-cli/src/lib/commands/user.ts
|
||||||
| try { | ||||||
| const adtClient = await getAdtClientV2(); | ||||||
|
|
||||||
| if (!query) { | ||||||
| // No query: show current user via systeminformation | ||||||
| console.log('🔄 Fetching current user...\n'); | ||||||
|
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
|
||||||
| const sysInfo = | ||||||
| (await adtClient.adt.core.http.systeminformation.getSystemInfo()) as Record< | ||||||
| string, | ||||||
| unknown | ||||||
| >; | ||||||
|
|
||||||
| if (options.json) { | ||||||
| console.log( | ||||||
| JSON.stringify( | ||||||
| { | ||||||
| userName: sysInfo.userName, | ||||||
| userFullName: sysInfo.userFullName, | ||||||
| systemID: sysInfo.systemID, | ||||||
| client: sysInfo.client, | ||||||
| }, | ||||||
| null, | ||||||
| 2, | ||||||
| ), | ||||||
| ); | ||||||
| } else { | ||||||
| console.log(`👤 Current User: ${sysInfo.userName}`); | ||||||
| if (sysInfo.userFullName) { | ||||||
| console.log(` Full Name: ${sysInfo.userFullName}`); | ||||||
|
Check warning on line 51 in packages/adt-cli/src/lib/commands/user.ts
|
||||||
| } | ||||||
| console.log( | ||||||
| ` System: ${sysInfo.systemID} (client ${sysInfo.client})`, | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| console.log('\n✅ Done!'); | ||||||
| return; | ||||||
| } | ||||||
|
qodo-code-review[bot] marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| // Check if query looks like an exact username (no wildcards) | ||||||
| const isExactLookup = !query.includes('*') && !query.includes('?'); | ||||||
|
|
||||||
| if (isExactLookup) { | ||||||
| // Get specific user | ||||||
| console.log(`🔍 Looking up user: ${query.toUpperCase()}...\n`); | ||||||
| const result = await adtClient.adt.system.users.get( | ||||||
| query.toUpperCase(), | ||||||
| ); | ||||||
|
|
||||||
| const entries = extractEntries(result); | ||||||
| if (entries.length === 0) { | ||||||
| console.log('No user found.'); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| if (options.json) { | ||||||
| console.log( | ||||||
| JSON.stringify( | ||||||
| entries.map((e) => ({ username: e.id, fullName: e.title })), | ||||||
| null, | ||||||
| 2, | ||||||
| ), | ||||||
| ); | ||||||
| } else { | ||||||
| for (const entry of entries) { | ||||||
| console.log(`👤 ${entry.id}`); | ||||||
| if (entry.title) console.log(` Full Name: ${entry.title}`); | ||||||
| if (entry.link?.[0]?.href) | ||||||
| console.log(` URI: ${entry.link[0].href}`); | ||||||
| } | ||||||
| } | ||||||
| } else { | ||||||
| // Search users | ||||||
| const maxcount = parseInt(options.max, 10); | ||||||
|
Check warning on line 96 in packages/adt-cli/src/lib/commands/user.ts
|
||||||
| console.log(`🔍 Searching users: "${query}" (max: ${maxcount})...\n`); | ||||||
| const result = await adtClient.adt.system.users.search({ | ||||||
| querystring: query, | ||||||
| maxcount, | ||||||
| }); | ||||||
|
|
||||||
| const entries = extractEntries(result); | ||||||
| if (entries.length === 0) { | ||||||
| console.log('No users found matching your query.'); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| if (options.json) { | ||||||
| console.log( | ||||||
| JSON.stringify( | ||||||
| entries.map((e) => ({ username: e.id, fullName: e.title })), | ||||||
| null, | ||||||
| 2, | ||||||
| ), | ||||||
| ); | ||||||
| } else { | ||||||
| console.log(`Found ${entries.length} user(s):\n`); | ||||||
| for (const entry of entries) { | ||||||
| console.log(` ${entry.id?.padEnd(12)} ${entry.title || ''}`); | ||||||
|
||||||
| console.log(` ${entry.id?.padEnd(12)} ${entry.title || ''}`); | |
| console.log(` ${(entry.id ?? '').padEnd(12)} ${entry.title || ''}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. The extractEntries helper now lives in the UserService and toUserInfoList() normalizes entries with username: e.id ?? '' before returning. The command display helper also uses (user.username ?? '').padEnd(12) so undefined IDs produce empty strings.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| /** | ||
| * ADT System Contracts - Aggregated | ||
| */ | ||
|
|
||
| export * from './users'; | ||
|
|
||
| import { usersContract, type UsersContract } from './users'; | ||
|
|
||
| export interface SystemContract { | ||
| users: UsersContract; | ||
| } | ||
|
|
||
| export const systemContract: SystemContract = { | ||
| users: usersContract, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** | ||
| * ADT System Users Contract | ||
| * | ||
| * User lookup and search operations. | ||
| * Path: /sap/bc/adt/system/users | ||
| */ | ||
|
|
||
| import { http } from '../../base'; | ||
| import { atomFeed } from '../../schemas'; | ||
|
|
||
| export const usersContract = { | ||
| /** | ||
| * Search for users by query string | ||
| * | ||
| * @param options.querystring - Search query (supports wildcards like *) | ||
| * @param options.maxcount - Maximum number of results | ||
| * @returns Atom feed with matching user entries (id = username, title = full name) | ||
| * | ||
| * @example | ||
| * const results = await client.system.users.search({ querystring: 'DEV*', maxcount: 10 }); | ||
| */ | ||
| search: (options: { querystring: string; maxcount?: number }) => | ||
| http.get('/sap/bc/adt/system/users', { | ||
| query: { | ||
| querystring: options.querystring, | ||
| ...(options.maxcount !== undefined && { maxcount: options.maxcount }), | ||
| }, | ||
| responses: { | ||
| 200: atomFeed, | ||
| }, | ||
| headers: { | ||
| Accept: 'application/atom+xml;type=feed', | ||
| }, | ||
| }), | ||
|
|
||
| /** | ||
| * Get a specific user by username | ||
| * | ||
| * @param username - SAP username (e.g., 'DEVELOPER') | ||
| * @returns Atom feed with a single user entry | ||
| * | ||
| * @example | ||
| * const user = await client.system.users.get('DEVELOPER'); | ||
| */ | ||
| get: (username: string) => | ||
| http.get(`/sap/bc/adt/system/users/${encodeURIComponent(username)}`, { | ||
| responses: { | ||
| 200: atomFeed, | ||
| }, | ||
| headers: { | ||
| Accept: 'application/atom+xml;type=feed', | ||
| }, | ||
| }), | ||
| }; | ||
|
|
||
| export type UsersContract = typeof usersContract; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /** | ||
| * System Users Contract Scenarios | ||
| */ | ||
|
|
||
| import { fixtures } from '@abapify/adt-fixtures'; | ||
| import { atomFeed } from '../../src/schemas'; | ||
| import { ContractScenario, runScenario, type ContractOperation } from './base'; | ||
| import { usersContract } from '../../src/adt/system/users'; | ||
|
|
||
| class UsersScenario extends ContractScenario { | ||
| readonly name = 'System Users'; | ||
|
|
||
| readonly operations: ContractOperation[] = [ | ||
| { | ||
| name: 'search users', | ||
| contract: () => | ||
| usersContract.search({ querystring: 'DEV*', maxcount: 10 }), | ||
| method: 'GET', | ||
| path: '/sap/bc/adt/system/users', | ||
| query: { querystring: 'DEV*', maxcount: 10 }, | ||
| headers: { | ||
| Accept: 'application/atom+xml;type=feed', | ||
| }, | ||
| response: { | ||
| status: 200, | ||
| schema: atomFeed, | ||
| fixture: fixtures.system.users.search, | ||
| }, | ||
| }, | ||
| { | ||
| name: 'get single user', | ||
| contract: () => usersContract.get('DEVELOPER'), | ||
| method: 'GET', | ||
| path: '/sap/bc/adt/system/users/DEVELOPER', | ||
| headers: { | ||
| Accept: 'application/atom+xml;type=feed', | ||
| }, | ||
| response: { | ||
| status: 200, | ||
| schema: atomFeed, | ||
| fixture: fixtures.system.users.single, | ||
| }, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| runScenario(new UsersScenario()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createCLI()now registers the newusercommand, butcli.test.tsonly asserts the program name. Since there is already a CLI-level test suite, add an assertion that theusercommand is present (e.g.,program.commandsincludes it) to prevent accidental removal/regression.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Added a test in
cli.test.tsthat assertsuseris inprogram.commands. Note: the CLI package has no vitest config or Nx test target yet, but the test is ready for when one is added.