Skip to content
Merged

``` #12

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
49 changes: 27 additions & 22 deletions packages/adk/tests/transport-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';

// Mock the ADT client
// Response must have 'root' wrapper to match TransportmanagmentSingleSchema
const mockTransportResponse = {
object_type: 'K',
name: 'DEVK900001',
type: 'RQRQ',
request: {
root: {
object_type: 'K',
name: 'DEVK900001',
type: 'RQRQ',
request: {
number: 'DEVK900001',
parent: '',
owner: 'DEVELOPER',
Expand Down Expand Up @@ -73,6 +75,7 @@ const mockTransportResponse = {
},
],
},
}, // close root
};

// Create mock client
Expand Down Expand Up @@ -240,24 +243,26 @@ describe('AdkTransport', () => {
it('should deduplicate objects across tasks', async () => {
// Create mock with duplicate object
const mockWithDuplicates = {
...mockTransportResponse,
request: {
...mockTransportResponse.request,
task: [
{
number: 'DEVK900002',
abap_object: [
{ pgmid: 'R3TR', type: 'CLAS', name: 'ZCL_SHARED' },
],
},
{
number: 'DEVK900003',
abap_object: [
{ pgmid: 'R3TR', type: 'CLAS', name: 'ZCL_SHARED' }, // Duplicate
{ pgmid: 'R3TR', type: 'PROG', name: 'ZTEST' },
],
},
],
root: {
...mockTransportResponse.root,
request: {
...mockTransportResponse.root.request,
task: [
{
number: 'DEVK900002',
abap_object: [
{ pgmid: 'R3TR', type: 'CLAS', name: 'ZCL_SHARED' },
],
},
{
number: 'DEVK900003',
abap_object: [
{ pgmid: 'R3TR', type: 'CLAS', name: 'ZCL_SHARED' }, // Duplicate
{ pgmid: 'R3TR', type: 'PROG', name: 'ZTEST' },
],
},
],
},
},
};

Expand Down
4 changes: 2 additions & 2 deletions packages/adt-cli/src/lib/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest';
import { createCLI } from './cli';

describe('ADT CLI', () => {
it('should create CLI program', () => {
const program = createCLI();
it('should create CLI program', async () => {
const program = await createCLI();
expect(program).toBeDefined();
expect(program.name()).toBe('adt');
});
Expand Down
7 changes: 3 additions & 4 deletions packages/adt-cli/src/lib/commands/cts/tr/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,14 @@ export const ctsCreateCommand = new Command('create')
}

// Create the transport via ADK layer
// ADK expects { services: { transports } } - client already has client.services
const adkCtx = { services: client.services };
const tr = await AdkTransportRequest.create(adkCtx, {
// ADK expects (options, ctx?) - ctx is AdkContext with client property
const tr = await AdkTransportRequest.create({
description: createOptions.description,
type: createOptions.type as 'K' | 'W',
target: createOptions.target,
project: createOptions.project,
owner: createOptions.owner,
});
}, { client });

if (options.json) {
console.log(JSON.stringify({
Expand Down
5 changes: 2 additions & 3 deletions packages/adt-cli/src/lib/commands/cts/tr/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,11 @@ export const ctsReleaseCommand = new Command('release')

// Step 1: Get transport via ADK
progress.step(`🔍 Getting transport ${transport}...`);
// ADK expects { services: { transports } } - client already has client.services
const adkCtx = { services: client.services };
// ADK expects (number, ctx?) - ctx is AdkContext with client property

let tr: AdkTransportRequest;
try {
tr = await AdkTransportRequest.get(adkCtx, transport);
tr = await AdkTransportRequest.get(transport, { client });
} catch (err) {
console.error(`❌ Transport ${transport} not found or not accessible`);
process.exit(1);
Expand Down
5 changes: 2 additions & 3 deletions packages/adt-cli/src/lib/commands/cts/tr/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ export const ctsSetCommand = new Command('set')

// Get transport via ADK
progress.step(`🔍 Getting transport ${transport}...`);
// ADK expects { services: { transports } } - client already has client.services
const adkCtx = { services: client.services };
const tr = await AdkTransportRequest.get(adkCtx, transport);
// ADK expects (number, ctx?) - ctx is AdkContext with client property
const tr = await AdkTransportRequest.get(transport, { client });
progress.done();

// Update using ADK (handles lock/unlock automatically)
Expand Down
7 changes: 5 additions & 2 deletions packages/adt-cli/src/lib/commands/cts/tree/config-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ export const treeConfigSetCommand = new Command('set')
// Get configurations list
const configsResponse = await client.adt.cts.transportrequests.searchconfiguration.configurations.get();

const configs = configsResponse?.configuration;
// Response has 'configurations' property (plural) containing config array
const configsData = configsResponse as { configurations?: unknown };
const configs = configsData?.configurations as Array<{ link?: { href?: string } }> | undefined;
if (!configs) {
console.log('\n❌ No search configuration found');
return;
Expand Down Expand Up @@ -179,9 +181,10 @@ export const treeConfigSetCommand = new Command('set')
console.log('\n🔄 Saving configuration...');
const configData = buildConfigurationData(newProps);

// Cast to unknown to bypass schema mismatch - TODO: align schema with actual API
await client.adt.cts.transportrequests.searchconfiguration.configurations.put(
configId,
configData
configData as unknown as Parameters<typeof client.adt.cts.transportrequests.searchconfiguration.configurations.put>[1]
);

console.log('✅ Configuration updated successfully');
Expand Down
24 changes: 20 additions & 4 deletions packages/adt-cli/src/lib/commands/cts/tree/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ export const treeConfigCommand = new Command('config')
// Get configurations list (typed)
const configsResponse = await client.adt.cts.transportrequests.searchconfiguration.configurations.get();

const configs = configsResponse?.configuration;
// Response has 'configurations' property (plural) containing config array
const configsData = configsResponse as { configurations?: unknown };
const configs = configsData?.configurations as Array<{ link?: { href?: string } }> | undefined;
if (!configs) {
console.log('\n📭 No search configuration found');
return;
Expand All @@ -220,10 +222,23 @@ export const treeConfigCommand = new Command('config')

// Extract config ID and fetch details using typed contract
const configId = extractConfigId(configUri);
const configDetails = await client.adt.cts.transportrequests.searchconfiguration.configurations.getById(configId);
const configDetailsRaw = await client.adt.cts.transportrequests.searchconfiguration.configurations.getById(configId);

// Response has nested structure - extract configuration object
// Cast to expected shape for property access
type ConfigDetails = {
configuration?: {
properties?: { property?: Array<{ key?: string; _text?: string; isMandatory?: boolean }> };
createdBy?: string;
createdAt?: string;
changedBy?: string;
changedAt?: string;
};
};
const configDetails = (configDetailsRaw as ConfigDetails)?.configuration;

// Convert properties to map for easy access
const properties = propertiesToMap(configDetails);
const properties = propertiesToMap(configDetails as Record<string, unknown>);

// Edit mode - launch interactive editor
if (options.edit) {
Expand All @@ -239,9 +254,10 @@ export const treeConfigCommand = new Command('config')
// PUT the configuration back using the typed contract
// Body type is Partial<Configuration> - we only send properties
// Note: CSRF token is auto-initialized by the adapter before write operations
// Cast to unknown to bypass schema mismatch - TODO: align schema with actual API
await client.adt.cts.transportrequests.searchconfiguration.configurations.put(
configId,
configData
configData as unknown as Parameters<typeof client.adt.cts.transportrequests.searchconfiguration.configurations.put>[1]
);
};

Expand Down
102 changes: 12 additions & 90 deletions packages/adt-cli/src/lib/commands/lock.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,25 @@
import { Command } from 'commander';
import { AdtClientImpl } from '@abapify/adt-client';

async function lockObject(objectName: string, options: any, command: any) {
const logger = command.parent?.logger;
// TODO: Migrate to v2 client when lock/unlock contracts are available
// This command requires v1-specific features: searchObjectsDetailed, lockObject
// See: docs/plans/active/2025-12-20-adt-cli-api-compatibility.md

try {
console.log(`🔒 Locking object: ${objectName}`);

// Create ADT client with logger (only for verbose mode)
const client = new AdtClientImpl({
logger: logger?.child({ component: 'cli' }),
});

// Search for the object using ADT client search function
console.log(`🔍 Searching for object...`);

const searchOptions = {
operation: 'quickSearch' as const,
query: objectName,
maxResults: 2,
};
const result = await client.repository.searchObjectsDetailed(searchOptions);
const searchResults = result.objects || [];

if (searchResults.length === 0) {
console.error(`❌ Object '${objectName}' not found in SAP system`);
process.exit(1);
}

if (searchResults.length > 1) {
console.error(
`❌ Multiple objects found for '${objectName}'. Please be more specific:`
);
searchResults.forEach((obj: any, index: number) => {
console.log(` ${index + 1}. ${obj.name} (${obj.type}) - ${obj.uri}`);
});
console.log(
`💡 Use a more specific name or add filters to narrow down the search`
);
process.exit(1);
}

const foundObject = searchResults[0];
const objectUri = foundObject.uri;

console.log(
`✅ Found: ${foundObject.name} (${foundObject.type}) - ${foundObject.description}`
);
console.log(`🔒 Attempting lock...`);

try {
const lockHandle = await client.repository.lockObject(objectUri);

console.log(`✅ SUCCESS! Object ${objectName} locked`);
console.log(`🔑 Lock handle: ${lockHandle}`);
} catch (error: any) {
const errorMessage = error?.message || String(error);
const statusCode = error?.statusCode || error?.context?.status;

console.error(`❌ Lock failed: ${errorMessage} (Status: ${statusCode})`);

// Show helpful error information
if (errorMessage.includes('currently editing')) {
console.log(
`💡 The object is already locked by another user or session`
);
console.log(
` - Check transaction SM12 in SAP GUI to see who has the lock`
);
console.log(
` - Use 'npx adt unlock ${objectName}' to force unlock if it's your lock`
);
} else if (errorMessage.includes('not found')) {
console.log(
`💡 The object might not exist or you don't have access to it`
);
} else {
console.log(`💡 The lock might have failed due to:`);
console.log(` - Insufficient permissions`);
console.log(` - Object is already locked`);
console.log(` - Network or system issues`);
}

process.exit(1);
}
} catch (error) {
console.error(`❌ Lock failed:`, error);
process.exit(1);
}
async function lockObject(objectName: string): Promise<void> {
// Stub implementation - command needs migration to v2 client
console.error(`❌ Lock command is temporarily disabled pending v2 client migration.`);
console.error(` Object: ${objectName}`);
console.error(`💡 Use SAP GUI transaction SE80 or SM12 to manage locks.`);
process.exit(1);
}

export function createLockCommand(): Command {
const command = new Command('lock');

command
.description('Lock a SAP object by name')
.description('Lock a SAP object by name (temporarily disabled)')
.argument('<objectName>', 'Name of the object to lock (e.g., ZIF_PETSTORE)')
.action(async (objectName: string, options: any, command: any) => {
await lockObject(objectName, options, command);
.action(async (objectName: string) => {
await lockObject(objectName);
});

return command;
Expand Down
Loading
Loading