abap-local-client 1.4.2 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/sso-sap-client.mjs +89 -4
package/package.json
CHANGED
package/sso-sap-client.mjs
CHANGED
|
@@ -269,6 +269,36 @@ async function handleHttpCall(callspec, httpsAgent) {
|
|
|
269
269
|
request_url_resolved: callspec.path.startsWith('http') ? callspec.path : `${sysCfg.baseUrl}${callspec.path.split('?')[0]}`, error: null }));
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
273
|
+
// ADT URL helper
|
|
274
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
275
|
+
|
|
276
|
+
const ADT_TYPE_PATHS = {
|
|
277
|
+
CLAS: '/sap/bc/adt/oo/classes',
|
|
278
|
+
CLASS: '/sap/bc/adt/oo/classes',
|
|
279
|
+
INTF: '/sap/bc/adt/oo/interfaces',
|
|
280
|
+
INTERFACE: '/sap/bc/adt/oo/interfaces',
|
|
281
|
+
PROG: '/sap/bc/adt/programs/programs',
|
|
282
|
+
REPORT: '/sap/bc/adt/programs/programs',
|
|
283
|
+
INCL: '/sap/bc/adt/programs/includes',
|
|
284
|
+
INCLUDE: '/sap/bc/adt/programs/includes',
|
|
285
|
+
TABL: '/sap/bc/adt/ddic/tables',
|
|
286
|
+
TABLE: '/sap/bc/adt/ddic/tables',
|
|
287
|
+
STRU: '/sap/bc/adt/ddic/structures',
|
|
288
|
+
DDLS: '/sap/bc/adt/ddic/ddl/sources',
|
|
289
|
+
CDS: '/sap/bc/adt/ddic/ddl/sources',
|
|
290
|
+
BDEF: '/sap/bc/adt/bo/behaviordefinitions',
|
|
291
|
+
SRVD: '/sap/bc/adt/bo/servicedefinitions',
|
|
292
|
+
SRVB: '/sap/bc/adt/bo/servicebindings',
|
|
293
|
+
FUNC: '/sap/bc/adt/functions/groups',
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
function buildAdtUnlockUrl(name, type, lockHandle) {
|
|
297
|
+
const basePath = ADT_TYPE_PATHS[type?.toUpperCase()];
|
|
298
|
+
if (!basePath) return null;
|
|
299
|
+
return `${basePath}/${name.toUpperCase()}?_action=UNLOCK&lockHandle=${encodeURIComponent(lockHandle)}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
272
302
|
// ════════════════════════════════════════════════════════════════════════════
|
|
273
303
|
// Lock Manager — IDENTICAL to websocket-client.js
|
|
274
304
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -288,6 +318,7 @@ class LockManager {
|
|
|
288
318
|
}
|
|
289
319
|
clearLock(objectName, objectType) { this.locks.delete(`${objectName.toUpperCase()}:${objectType.toUpperCase()}`); }
|
|
290
320
|
clearAll() { const c = this.locks.size; this.locks.clear(); if (c > 0) console.log(` 🧹 All locks cleared (${c} lock(s))`); }
|
|
321
|
+
getAll() { return Array.from(this.locks.entries()).map(([key, lock]) => { const [name, type] = key.split(':'); return { name, type, ...lock }; }); }
|
|
291
322
|
getStats() { return { activeLocks: this.locks.size, locks: Array.from(this.locks.keys()) }; }
|
|
292
323
|
}
|
|
293
324
|
|
|
@@ -658,6 +689,7 @@ class WebSocketClient {
|
|
|
658
689
|
if (!sys) { this.sendResponse(requestId, false, { error: `Unknown system: ${sid}` }); return; }
|
|
659
690
|
this.currentSystemId = sid;
|
|
660
691
|
this.lockManager.clearAll();
|
|
692
|
+
sessions.delete(sid); // force re-create session with fresh config on every switch
|
|
661
693
|
const ok = await this.initSapClient();
|
|
662
694
|
if (!ok) { this.sendResponse(requestId, false, { error: `Failed to connect to ${sid}. Check RFC/SNC config and logs.` }); return; }
|
|
663
695
|
this.sendResponse(requestId, true, [{ success: true, data: { system: sid, authMode: sys.authMode } }]);
|
|
@@ -726,6 +758,41 @@ class WebSocketClient {
|
|
|
726
758
|
}
|
|
727
759
|
for (let i = 0; i < callspecs.length; i++) {
|
|
728
760
|
const cs = callspecs[i];
|
|
761
|
+
|
|
762
|
+
// ── Special client-side commands ────────────────────────────────────
|
|
763
|
+
if (cs.command) {
|
|
764
|
+
if (cs.command === 'unlock_all') {
|
|
765
|
+
console.log(`\n 📋 Step: unlock_all (client-side)`);
|
|
766
|
+
const allLocks = this.lockManager.getAll();
|
|
767
|
+
console.log(` 🔓 Unlocking ${allLocks.length} cached lock(s)...`);
|
|
768
|
+
let unlocked = 0, failed = 0;
|
|
769
|
+
for (const lock of allLocks) {
|
|
770
|
+
const unlockUrl = buildAdtUnlockUrl(lock.name, lock.type, lock.lockHandle);
|
|
771
|
+
if (!unlockUrl) { console.log(` ⚠️ No ADT path for type ${lock.type} — skipping`); failed++; continue; }
|
|
772
|
+
const r = await this.sapClient.executeCallspec({ method: 'DELETE', url: unlockUrl, headers: {} });
|
|
773
|
+
if (r.success) { this.lockManager.clearLock(lock.name, lock.type); unlocked++; console.log(` ✅ Unlocked ${lock.type}:${lock.name}`); }
|
|
774
|
+
else { failed++; console.log(` ⚠️ Failed to unlock ${lock.type}:${lock.name}: ${r.error || r.status}`); }
|
|
775
|
+
}
|
|
776
|
+
results.push({ success: true, status: 200, data: { unlocked, failed, total: allLocks.length } });
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (cs.command === 'force_unlock') {
|
|
780
|
+
const { objectName, objectType, lockHandle } = cs;
|
|
781
|
+
console.log(`\n 📋 Step: force_unlock ${objectType}:${objectName}`);
|
|
782
|
+
const handle = lockHandle || this.lockManager.getLock(objectName, objectType)?.lockHandle || '';
|
|
783
|
+
const unlockUrl = buildAdtUnlockUrl(objectName, objectType, handle);
|
|
784
|
+
if (!unlockUrl) { results.push({ success: false, status: 0, error: `Unknown object type: ${objectType}` }); allSuccessful = false; break; }
|
|
785
|
+
const r = await this.sapClient.executeCallspec({ method: 'DELETE', url: unlockUrl, headers: {} });
|
|
786
|
+
if (r.success) this.lockManager.clearLock(objectName, objectType);
|
|
787
|
+
results.push(r);
|
|
788
|
+
if (!r.success) { allSuccessful = false; break; }
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
console.log(`\n 📋 Step: unknown command "${cs.command}" — skipping`);
|
|
792
|
+
results.push({ success: true, status: 200, data: { skipped: true, command: cs.command } });
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
|
|
729
796
|
console.log(`\n 📋 Step: ${cs.name || cs.step}`);
|
|
730
797
|
const isLockOp = cs.name === 'LOCK' || cs.url?.includes('_action=LOCK');
|
|
731
798
|
if (isLockOp && objectName && objectType) {
|
|
@@ -751,16 +818,34 @@ class WebSocketClient {
|
|
|
751
818
|
console.log(` 🔓 Injected lockHandle into UNLOCK URL from cache: ${cachedLock.lockHandle.substring(0,10)}...`);
|
|
752
819
|
}
|
|
753
820
|
}
|
|
821
|
+
const isUnlockOp = pcs.url?.includes('_action=UNLOCK');
|
|
754
822
|
const result = await this.sapClient.executeCallspec(pcs);
|
|
755
823
|
results.push(result);
|
|
756
|
-
if (result.success
|
|
757
|
-
if (result.data
|
|
824
|
+
if (result.success) {
|
|
825
|
+
if (result.data?.lockHandle) {
|
|
758
826
|
extractedValues.lockHandle = result.data.lockHandle;
|
|
759
827
|
if (isLockOp && objectName && objectType) this.lockManager.saveLock(objectName, objectType, result.data.lockHandle, result.data.transportRequest || '');
|
|
760
828
|
}
|
|
761
|
-
if (result.data
|
|
829
|
+
if (result.data?.transportRequest) extractedValues.transportRequest = result.data.transportRequest;
|
|
830
|
+
// After successful UNLOCK, clear the cached handle — it is now invalid on SAP
|
|
831
|
+
if (isUnlockOp && objectName && objectType) {
|
|
832
|
+
this.lockManager.clearLock(objectName, objectType);
|
|
833
|
+
console.log(` 🗑️ Lock handle cleared from cache after successful UNLOCK of ${objectType}:${objectName}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (!result.success) {
|
|
837
|
+
// 423 with an invalid lock handle means the cached handle is stale — clear it so the
|
|
838
|
+
// next invocation does a fresh LOCK instead of reusing the dead handle
|
|
839
|
+
if (result.status === 423 && objectName && objectType) {
|
|
840
|
+
const stale = this.lockManager.getLock(objectName, objectType);
|
|
841
|
+
if (stale) {
|
|
842
|
+
this.lockManager.clearLock(objectName, objectType);
|
|
843
|
+
console.log(` 🗑️ Stale lock handle cleared from cache after 423 for ${objectType}:${objectName}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
allSuccessful = false;
|
|
847
|
+
break;
|
|
762
848
|
}
|
|
763
|
-
if (!result.success) { allSuccessful = false; break; }
|
|
764
849
|
}
|
|
765
850
|
return { results, allSuccessful };
|
|
766
851
|
} else {
|