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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sso-sap-client.mjs +88 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abap-local-client",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "SAP Local Client — WebSocket bridge with Basic, SPNEGO, X.509, SAML & SNC authentication",
5
5
  "main": "sso-sap-client.mjs",
6
6
  "type": "module",
@@ -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 && result.data) {
758
- if (result.data.lockHandle) {
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.transportRequest) extractedValues.transportRequest = result.data.transportRequest;
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 {