Skip to content

Commit 431e4e1

Browse files
committed
feat(adt): add user lookup command and system contract
Add `user` command to CLI for user lookups. Introduce `system` module in ADT contracts with user search/retrieval endpoints. Add `atomFeed` schema for Atom feed responses. Register system user fixtures (single, search). Re-export generated schemas in alphabetical order.
1 parent 72c6578 commit 431e4e1

17 files changed

Lines changed: 582 additions & 12 deletions

File tree

packages/adt-cli/src/lib/cli.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
lockCommand,
2525
locksCommand,
2626
checkCommand,
27+
userCommand,
2728
} from './commands';
2829
import { refreshCommand } from './commands/auth/refresh';
2930
// Deploy command moved to @abapify/adt-export plugin
@@ -217,6 +218,9 @@ export async function createCLI(options?: {
217218
// Check command (syntax check / checkruns)
218219
program.addCommand(checkCommand);
219220

221+
// User lookup command
222+
program.addCommand(userCommand);
223+
220224
// REPL - Interactive hypermedia navigator
221225
program.addCommand(createReplCommand());
222226

packages/adt-cli/src/lib/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export { unlockCommand } from './unlock';
2424
export { lockCommand } from './lock';
2525
export { locksCommand } from './locks';
2626
export { checkCommand } from './check';
27+
export { userCommand } from './user';
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Command } from 'commander';
2+
import { getAdtClientV2 } from '../utils/adt-client-v2';
3+
4+
/** Extract entries from atomFeed parsed response */
5+
function extractEntries(
6+
data: unknown,
7+
): { id?: string; title?: string; link?: { href: string }[] }[] {
8+
const feed = data as Record<string, unknown>;
9+
const feedData = feed.feed as Record<string, unknown> | undefined;
10+
if (!feedData) return [];
11+
12+
const rawEntries = feedData.entry;
13+
if (!rawEntries) return [];
14+
return Array.isArray(rawEntries) ? rawEntries : [rawEntries];
15+
}
16+
17+
export const userCommand = new Command('user')
18+
.description('Look up SAP system users')
19+
.argument('[query]', 'Username or search query (supports wildcards like *)')
20+
.option('-m, --max <number>', 'Maximum number of results', '50')
21+
.option('--json', 'Output results as JSON')
22+
.action(async (query: string | undefined, options) => {
23+
try {
24+
const adtClient = await getAdtClientV2();
25+
26+
if (!query) {
27+
// No query: show current user via systeminformation
28+
console.log('🔄 Fetching current user...\n');
29+
const sysInfo =
30+
(await adtClient.adt.core.http.systeminformation.getSystemInfo()) as Record<
31+
string,
32+
unknown
33+
>;
34+
35+
if (options.json) {
36+
console.log(
37+
JSON.stringify(
38+
{
39+
userName: sysInfo.userName,
40+
userFullName: sysInfo.userFullName,
41+
systemID: sysInfo.systemID,
42+
client: sysInfo.client,
43+
},
44+
null,
45+
2,
46+
),
47+
);
48+
} else {
49+
console.log(`👤 Current User: ${sysInfo.userName}`);
50+
if (sysInfo.userFullName) {
51+
console.log(` Full Name: ${sysInfo.userFullName}`);
52+
}
53+
console.log(
54+
` System: ${sysInfo.systemID} (client ${sysInfo.client})`,
55+
);
56+
}
57+
58+
console.log('\n✅ Done!');
59+
return;
60+
}
61+
62+
// Check if query looks like an exact username (no wildcards)
63+
const isExactLookup = !query.includes('*') && !query.includes('?');
64+
65+
if (isExactLookup) {
66+
// Get specific user
67+
console.log(`🔍 Looking up user: ${query.toUpperCase()}...\n`);
68+
const result = await adtClient.adt.system.users.get(
69+
query.toUpperCase(),
70+
);
71+
72+
const entries = extractEntries(result);
73+
if (entries.length === 0) {
74+
console.log('No user found.');
75+
return;
76+
}
77+
78+
if (options.json) {
79+
console.log(
80+
JSON.stringify(
81+
entries.map((e) => ({ username: e.id, fullName: e.title })),
82+
null,
83+
2,
84+
),
85+
);
86+
} else {
87+
for (const entry of entries) {
88+
console.log(`👤 ${entry.id}`);
89+
if (entry.title) console.log(` Full Name: ${entry.title}`);
90+
if (entry.link?.[0]?.href)
91+
console.log(` URI: ${entry.link[0].href}`);
92+
}
93+
}
94+
} else {
95+
// Search users
96+
const maxcount = parseInt(options.max, 10);
97+
console.log(`🔍 Searching users: "${query}" (max: ${maxcount})...\n`);
98+
const result = await adtClient.adt.system.users.search({
99+
querystring: query,
100+
maxcount,
101+
});
102+
103+
const entries = extractEntries(result);
104+
if (entries.length === 0) {
105+
console.log('No users found matching your query.');
106+
return;
107+
}
108+
109+
if (options.json) {
110+
console.log(
111+
JSON.stringify(
112+
entries.map((e) => ({ username: e.id, fullName: e.title })),
113+
null,
114+
2,
115+
),
116+
);
117+
} else {
118+
console.log(`Found ${entries.length} user(s):\n`);
119+
for (const entry of entries) {
120+
console.log(` ${entry.id?.padEnd(12)} ${entry.title || ''}`);
121+
}
122+
}
123+
}
124+
125+
console.log('\n✅ Done!');
126+
} catch (error) {
127+
console.error(
128+
'❌ User lookup failed:',
129+
error instanceof Error ? error.message : String(error),
130+
);
131+
if (error instanceof Error && error.stack) {
132+
console.error('\nStack trace:', error.stack);
133+
}
134+
process.exit(1);
135+
}
136+
});

packages/adt-contracts/src/adt/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from './repository';
1313
export * from './programs';
1414
export * from './functions';
1515
export * from './ddic';
16+
export * from './system';
1617

1718
/**
1819
* Complete ADT Contract
@@ -31,6 +32,7 @@ import {
3132
} from './programs';
3233
import { functionsContract, type FunctionsContract } from './functions';
3334
import { ddicContract, type DdicContract } from './ddic';
35+
import { systemContract, type SystemContract } from './system';
3436

3537
/**
3638
* Explicit type to avoid TS7056 "inferred type exceeds maximum length"
@@ -47,6 +49,7 @@ export interface AdtContract {
4749
programs: ProgramsModuleContract;
4850
functions: FunctionsContract;
4951
ddic: DdicContract;
52+
system: SystemContract;
5053
}
5154

5255
export const adtContract: AdtContract = {
@@ -61,6 +64,7 @@ export const adtContract: AdtContract = {
6164
programs: programsModuleContract,
6265
functions: functionsContract,
6366
ddic: ddicContract,
67+
system: systemContract,
6468
};
6569

6670
// Import RestClient from base for client type definition
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* ADT System Contracts - Aggregated
3+
*/
4+
5+
export * from './users';
6+
7+
import { usersContract, type UsersContract } from './users';
8+
9+
export interface SystemContract {
10+
users: UsersContract;
11+
}
12+
13+
export const systemContract: SystemContract = {
14+
users: usersContract,
15+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* ADT System Users Contract
3+
*
4+
* User lookup and search operations.
5+
* Path: /sap/bc/adt/system/users
6+
*/
7+
8+
import { http } from '../../base';
9+
import { atomFeed } from '../../schemas';
10+
11+
export const usersContract = {
12+
/**
13+
* Search for users by query string
14+
*
15+
* @param options.querystring - Search query (supports wildcards like *)
16+
* @param options.maxcount - Maximum number of results
17+
* @returns Atom feed with matching user entries (id = username, title = full name)
18+
*
19+
* @example
20+
* const results = await client.system.users.search({ querystring: 'DEV*', maxcount: 10 });
21+
*/
22+
search: (options: { querystring: string; maxcount?: number }) =>
23+
http.get('/sap/bc/adt/system/users', {
24+
query: {
25+
querystring: options.querystring,
26+
...(options.maxcount !== undefined && { maxcount: options.maxcount }),
27+
},
28+
responses: {
29+
200: atomFeed,
30+
},
31+
headers: {
32+
Accept: 'application/atom+xml;type=feed',
33+
},
34+
}),
35+
36+
/**
37+
* Get a specific user by username
38+
*
39+
* @param username - SAP username (e.g., 'DEVELOPER')
40+
* @returns Atom feed with a single user entry
41+
*
42+
* @example
43+
* const user = await client.system.users.get('DEVELOPER');
44+
*/
45+
get: (username: string) =>
46+
http.get(`/sap/bc/adt/system/users/${encodeURIComponent(username)}`, {
47+
responses: {
48+
200: atomFeed,
49+
},
50+
headers: {
51+
Accept: 'application/atom+xml;type=feed',
52+
},
53+
}),
54+
};
55+
56+
export type UsersContract = typeof usersContract;

packages/adt-contracts/src/generated/schemas.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,33 @@ export const atctagdescription = toSpeciSchema(adtSchemas.atctagdescription);
2424
export const atcworklist = toSpeciSchema(adtSchemas.atcworklist);
2525
export const atom = toSpeciSchema(adtSchemas.atom);
2626
export const atomExtended = toSpeciSchema(adtSchemas.atomExtended);
27+
export const atomFeed = toSpeciSchema(adtSchemas.atomFeed);
28+
export const aunitResult = toSpeciSchema(adtSchemas.aunitResult);
29+
export const aunitRun = toSpeciSchema(adtSchemas.aunitRun);
30+
export const blueSource = toSpeciSchema(adtSchemas.blueSource);
2731
export const checklist = toSpeciSchema(adtSchemas.checklist);
2832
export const checkrun = toSpeciSchema(adtSchemas.checkrun);
2933
export const classes = toSpeciSchema(adtSchemas.classes);
3034
export const configuration = toSpeciSchema(adtSchemas.configuration);
3135
export const configurations = toSpeciSchema(adtSchemas.configurations);
36+
export const dataelementWrapper = toSpeciSchema(adtSchemas.dataelementWrapper);
37+
export const dataelements = toSpeciSchema(adtSchemas.dataelements);
3238
export const debuggerSchema = toSpeciSchema(adtSchemas.debuggerSchema);
3339
export const discovery = toSpeciSchema(adtSchemas.discovery);
40+
export const domain = toSpeciSchema(adtSchemas.domain);
41+
export const exception = toSpeciSchema(adtSchemas.exception);
42+
export const fincludes = toSpeciSchema(adtSchemas.fincludes);
43+
export const fmodules = toSpeciSchema(adtSchemas.fmodules);
44+
export const groups = toSpeciSchema(adtSchemas.groups);
3445
export const http = toSpeciSchema(adtSchemas.http);
3546
export const interfaces = toSpeciSchema(adtSchemas.interfaces);
3647
export const log = toSpeciSchema(adtSchemas.log);
3748
export const logpoint = toSpeciSchema(adtSchemas.logpoint);
3849
export const packagesV1 = toSpeciSchema(adtSchemas.packagesV1);
50+
export const programs = toSpeciSchema(adtSchemas.programs);
3951
export const quickfixes = toSpeciSchema(adtSchemas.quickfixes);
52+
export const tablesettings = toSpeciSchema(adtSchemas.tablesettings);
53+
export const tabletype = toSpeciSchema(adtSchemas.tabletype);
4054
export const templatelink = toSpeciSchema(adtSchemas.templatelink);
4155
export const templatelinkExtended = toSpeciSchema(
4256
adtSchemas.templatelinkExtended,
@@ -51,18 +65,6 @@ export const transportmanagmentSingle = toSpeciSchema(
5165
adtSchemas.transportmanagmentSingle,
5266
);
5367
export const transportsearch = toSpeciSchema(adtSchemas.transportsearch);
54-
export const aunitRun = toSpeciSchema(adtSchemas.aunitRun);
55-
export const aunitResult = toSpeciSchema(adtSchemas.aunitResult);
56-
export const programs = toSpeciSchema(adtSchemas.programs);
57-
export const groups = toSpeciSchema(adtSchemas.groups);
58-
export const fmodules = toSpeciSchema(adtSchemas.fmodules);
59-
export const fincludes = toSpeciSchema(adtSchemas.fincludes);
60-
export const domain = toSpeciSchema(adtSchemas.domain);
61-
export const dataelements = toSpeciSchema(adtSchemas.dataelements);
62-
export const dataelementWrapper = toSpeciSchema(adtSchemas.dataelementWrapper);
63-
export const tabletype = toSpeciSchema(adtSchemas.tabletype);
64-
export const tablesettings = toSpeciSchema(adtSchemas.tablesettings);
65-
export const blueSource = toSpeciSchema(adtSchemas.blueSource);
6668

6769
// ============================================================================
6870
// JSON Schemas (re-exported directly - they use zod, not ts-xsd)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* System Users Contract Scenarios
3+
*/
4+
5+
import { fixtures } from '@abapify/adt-fixtures';
6+
import { atomFeed } from '../../src/schemas';
7+
import { ContractScenario, runScenario, type ContractOperation } from './base';
8+
import { usersContract } from '../../src/adt/system/users';
9+
10+
class UsersScenario extends ContractScenario {
11+
readonly name = 'System Users';
12+
13+
readonly operations: ContractOperation[] = [
14+
{
15+
name: 'search users',
16+
contract: () =>
17+
usersContract.search({ querystring: 'DEV*', maxcount: 10 }),
18+
method: 'GET',
19+
path: '/sap/bc/adt/system/users',
20+
query: { querystring: 'DEV*', maxcount: 10 },
21+
headers: {
22+
Accept: 'application/atom+xml;type=feed',
23+
},
24+
response: {
25+
status: 200,
26+
schema: atomFeed,
27+
fixture: fixtures.system.users.search,
28+
},
29+
},
30+
{
31+
name: 'get single user',
32+
contract: () => usersContract.get('DEVELOPER'),
33+
method: 'GET',
34+
path: '/sap/bc/adt/system/users/DEVELOPER',
35+
headers: {
36+
Accept: 'application/atom+xml;type=feed',
37+
},
38+
response: {
39+
status: 200,
40+
schema: atomFeed,
41+
fixture: fixtures.system.users.single,
42+
},
43+
},
44+
];
45+
}
46+
47+
runScenario(new UsersScenario());

packages/adt-fixtures/src/fixtures/registry.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export const registry = {
4444
quickSearch: 'repository/search/quickSearch.xml',
4545
},
4646
},
47+
system: {
48+
users: {
49+
single: 'system/users/single.xml',
50+
search: 'system/users/search.xml',
51+
},
52+
},
4753
ddic: {
4854
tabl: {
4955
structure: 'ddic/tabl/structure.tabl.xml',

0 commit comments

Comments
 (0)