abap-local-client 1.4.3 → 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 +88 -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
|
|
|
@@ -727,6 +758,41 @@ class WebSocketClient {
|
|
|
727
758
|
}
|
|
728
759
|
for (let i = 0; i < callspecs.length; i++) {
|
|
729
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
|
+
|
|
730
796
|
console.log(`\n 📋 Step: ${cs.name || cs.step}`);
|
|
731
797
|
const isLockOp = cs.name === 'LOCK' || cs.url?.includes('_action=LOCK');
|
|
732
798
|
if (isLockOp && objectName && objectType) {
|
|
@@ -752,16 +818,34 @@ class WebSocketClient {
|
|
|
752
818
|
console.log(` 🔓 Injected lockHandle into UNLOCK URL from cache: ${cachedLock.lockHandle.substring(0,10)}...`);
|
|
753
819
|
}
|
|
754
820
|
}
|
|
821
|
+
const isUnlockOp = pcs.url?.includes('_action=UNLOCK');
|
|
755
822
|
const result = await this.sapClient.executeCallspec(pcs);
|
|
756
823
|
results.push(result);
|
|
757
|
-
if (result.success
|
|
758
|
-
if (result.data
|
|
824
|
+
if (result.success) {
|
|
825
|
+
if (result.data?.lockHandle) {
|
|
759
826
|
extractedValues.lockHandle = result.data.lockHandle;
|
|
760
827
|
if (isLockOp && objectName && objectType) this.lockManager.saveLock(objectName, objectType, result.data.lockHandle, result.data.transportRequest || '');
|
|
761
828
|
}
|
|
762
|
-
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;
|
|
763
848
|
}
|
|
764
|
-
if (!result.success) { allSuccessful = false; break; }
|
|
765
849
|
}
|
|
766
850
|
return { results, allSuccessful };
|
|
767
851
|
} else {
|