@abapify/adt-locks 0.4.0 → 0.4.1
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/dist/index.d.mts +183 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +226 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +2 -2
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* ADT Lock Types
|
|
4
|
+
*
|
|
5
|
+
* Shared type definitions for lock handles and persisted lock entries.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Lock handle returned by SAP `_action=LOCK` operations.
|
|
9
|
+
*
|
|
10
|
+
* The handle is required for subsequent PUT (save) and `_action=UNLOCK` calls.
|
|
11
|
+
*/
|
|
12
|
+
interface LockHandle {
|
|
13
|
+
/** Opaque lock token from SAP (LOCK_HANDLE) */
|
|
14
|
+
handle: string;
|
|
15
|
+
/** Transport request assigned to this object (CORRNR) */
|
|
16
|
+
correlationNumber?: string;
|
|
17
|
+
/** User who owns the transport (CORRUSER) */
|
|
18
|
+
correlationUser?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A persisted lock entry — written to the lock store so handles survive process crashes.
|
|
22
|
+
*/
|
|
23
|
+
interface LockEntry {
|
|
24
|
+
/** ADT object URI (e.g., /sap/bc/adt/oo/classes/zcl_my_class) */
|
|
25
|
+
objectUri: string;
|
|
26
|
+
/** Human-readable object name */
|
|
27
|
+
objectName: string;
|
|
28
|
+
/** ADT object type (e.g., CLAS/OC, INTF/OI, DTEL) */
|
|
29
|
+
objectType?: string;
|
|
30
|
+
/** The lock handle returned by SAP */
|
|
31
|
+
lockHandle: string;
|
|
32
|
+
/** Transport request from lock response */
|
|
33
|
+
transport?: string;
|
|
34
|
+
/** SAP system host */
|
|
35
|
+
host?: string;
|
|
36
|
+
/** ISO timestamp when lock was acquired */
|
|
37
|
+
lockedAt: string;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/store.d.ts
|
|
41
|
+
/**
|
|
42
|
+
* Lock store interface — opt-in persistence for ADT lock handles.
|
|
43
|
+
*
|
|
44
|
+
* Implementations can be file-based, in-memory, or backed by any storage.
|
|
45
|
+
*/
|
|
46
|
+
interface LockStore {
|
|
47
|
+
/** Persist a lock entry after successful LOCK */
|
|
48
|
+
register(entry: LockEntry): void;
|
|
49
|
+
/** Remove a lock entry after successful UNLOCK */
|
|
50
|
+
deregister(objectUri: string): void;
|
|
51
|
+
/** List all persisted lock entries */
|
|
52
|
+
list(): LockEntry[];
|
|
53
|
+
/** Remove all entries */
|
|
54
|
+
clear(): void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* File-based lock store.
|
|
58
|
+
*
|
|
59
|
+
* Persists lock entries to a JSON file (default: `~/.adt/locks.json`).
|
|
60
|
+
* Each entry is keyed by objectUri — re-locking the same URI overwrites.
|
|
61
|
+
* File permissions are 0o600 (owner read/write only).
|
|
62
|
+
*/
|
|
63
|
+
declare class FileLockStore implements LockStore {
|
|
64
|
+
private readonly filePath;
|
|
65
|
+
constructor(baseDir?: string);
|
|
66
|
+
register(entry: LockEntry): void;
|
|
67
|
+
deregister(objectUri: string): void;
|
|
68
|
+
list(): LockEntry[];
|
|
69
|
+
clear(): void;
|
|
70
|
+
private load;
|
|
71
|
+
private save;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/service.d.ts
|
|
75
|
+
/** Minimal client interface — anything with an authenticated fetch() */
|
|
76
|
+
interface LockClient {
|
|
77
|
+
fetch(url: string, options?: {
|
|
78
|
+
method?: string;
|
|
79
|
+
headers?: Record<string, string>;
|
|
80
|
+
body?: string;
|
|
81
|
+
}): Promise<unknown>;
|
|
82
|
+
}
|
|
83
|
+
interface LockServiceOptions {
|
|
84
|
+
/** Optional lock store for persistence */
|
|
85
|
+
store?: LockStore;
|
|
86
|
+
}
|
|
87
|
+
interface LockOptions {
|
|
88
|
+
/** Transport request */
|
|
89
|
+
transport?: string;
|
|
90
|
+
/** Object name (for store metadata) */
|
|
91
|
+
objectName?: string;
|
|
92
|
+
/** Object type (for store metadata) */
|
|
93
|
+
objectType?: string;
|
|
94
|
+
}
|
|
95
|
+
interface UnlockOptions {
|
|
96
|
+
/** Lock handle — REQUIRED. SAP always requires an explicit handle for unlock. */
|
|
97
|
+
lockHandle: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse SAP lock response XML to extract lock handle and correlation info.
|
|
101
|
+
*
|
|
102
|
+
* Response format:
|
|
103
|
+
* ```xml
|
|
104
|
+
* <asx:abap>...<DATA>
|
|
105
|
+
* <LOCK_HANDLE>xxx</LOCK_HANDLE>
|
|
106
|
+
* <CORRNR>yyy</CORRNR>
|
|
107
|
+
* <CORRUSER>zzz</CORRUSER>
|
|
108
|
+
* </DATA>...</asx:abap>
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare function parseLockResponse(xml: string): LockHandle;
|
|
112
|
+
interface LockService {
|
|
113
|
+
/** Acquire a lock on an ADT object */
|
|
114
|
+
lock(objectUri: string, options?: LockOptions): Promise<LockHandle>;
|
|
115
|
+
/** Release a lock on an ADT object (lock handle required) */
|
|
116
|
+
unlock(objectUri: string, options: UnlockOptions): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* Force-unlock: acquire a lock to recover the handle, then immediately unlock.
|
|
119
|
+
* Works when the object is locked by the current user but the handle was lost
|
|
120
|
+
* (e.g., crashed session). If the object is locked by another user, the lock
|
|
121
|
+
* call will fail with 403/409.
|
|
122
|
+
*/
|
|
123
|
+
forceUnlock(objectUri: string, options?: LockOptions): Promise<void>;
|
|
124
|
+
/** List all persisted lock entries (from store) */
|
|
125
|
+
list(): LockEntry[];
|
|
126
|
+
/** Try to UNLOCK every persisted entry, removing successful ones from store */
|
|
127
|
+
cleanup(): Promise<{
|
|
128
|
+
ok: number;
|
|
129
|
+
failed: number;
|
|
130
|
+
}>;
|
|
131
|
+
/** Clear the store without sending any UNLOCK requests */
|
|
132
|
+
clear(): void;
|
|
133
|
+
}
|
|
134
|
+
declare function createLockService(client: LockClient, serviceOptions?: LockServiceOptions): LockService;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/batch/batch-lock-session.d.ts
|
|
137
|
+
interface BatchLockTarget {
|
|
138
|
+
/** ADT object URI — e.g. `/sap/bc/adt/oo/classes/zcl_foo` */
|
|
139
|
+
objectUri: string;
|
|
140
|
+
/** Per-target lock options (transport, object name/type for the store). */
|
|
141
|
+
options?: LockOptions;
|
|
142
|
+
}
|
|
143
|
+
interface BatchLockAcquisition {
|
|
144
|
+
target: BatchLockTarget;
|
|
145
|
+
handle: LockHandle;
|
|
146
|
+
}
|
|
147
|
+
interface BatchReleaseResult {
|
|
148
|
+
released: number;
|
|
149
|
+
failed: Array<{
|
|
150
|
+
objectUri: string;
|
|
151
|
+
error: string;
|
|
152
|
+
}>;
|
|
153
|
+
}
|
|
154
|
+
interface BatchLockSessionOptions {
|
|
155
|
+
/**
|
|
156
|
+
* When true, `begin()` stops on the first failure and rolls back locks
|
|
157
|
+
* already acquired in this session. When false (rare), errors are
|
|
158
|
+
* collected and returned but already-acquired locks are kept — use only
|
|
159
|
+
* for diagnostic tooling. Default: true.
|
|
160
|
+
*/
|
|
161
|
+
rollbackOnError?: boolean;
|
|
162
|
+
}
|
|
163
|
+
interface BatchLockSession {
|
|
164
|
+
/** Acquire all configured locks. Rollback on failure (by default). */
|
|
165
|
+
begin(): Promise<BatchLockAcquisition[]>;
|
|
166
|
+
/** Release every currently held handle. Safe to call multiple times. */
|
|
167
|
+
release(): Promise<BatchReleaseResult>;
|
|
168
|
+
/** Snapshot of currently held handles. */
|
|
169
|
+
handles(): ReadonlyArray<BatchLockAcquisition>;
|
|
170
|
+
/** Has `begin()` completed successfully? */
|
|
171
|
+
readonly active: boolean;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Create a batch lock session over a set of targets.
|
|
175
|
+
*
|
|
176
|
+
* The session does NOT auto-begin; callers drive the lifecycle explicitly so
|
|
177
|
+
* error handling stays visible at the call-site (`try { begin(); … } finally
|
|
178
|
+
* { release(); }`).
|
|
179
|
+
*/
|
|
180
|
+
declare function createBatchLockSession(lockService: Pick<LockService, 'lock' | 'unlock'>, targets: BatchLockTarget[], options?: BatchLockSessionOptions): BatchLockSession;
|
|
181
|
+
//#endregion
|
|
182
|
+
export { type BatchLockAcquisition, type BatchLockSession, type BatchLockSessionOptions, type BatchLockTarget, type BatchReleaseResult, FileLockStore, type LockClient, type LockEntry, type LockHandle, type LockOptions, type LockService, type LockStore, type UnlockOptions, createBatchLockSession, createLockService, parseLockResponse };
|
|
183
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/store.ts","../src/service.ts","../src/batch/batch-lock-session.ts"],"mappings":";;AAWA;;;;;;;;;UAAiB,UAAA;EAYS;EAVxB,MAAA;EAUwB;EARxB,iBAAA;EAYA;EAVA,eAAA;AAAA;;;;UAMe,SAAA;EAcP;EAZR,SAAA;;EAEA,UAAA;ECVe;EDYf,UAAA;;EAEA,UAAA;ECZA;EDcA,SAAA;ECdS;EDgBT,IAAA;ECdW;EDgBX,QAAA;AAAA;;;;;;AAdF;;UCNiB,SAAA;EDMS;ECJxB,QAAA,CAAS,KAAA,EAAO,SAAA;EDQhB;ECNA,UAAA,CAAW,SAAA;EDUX;ECRA,IAAA,IAAQ,SAAA;EDYR;ECVA,KAAA;AAAA;;;;;AARF;;;cAkBa,aAAA,YAAyB,SAAA;EAAA,iBACnB,QAAA;cAEL,OAAA;EAKZ,QAAA,CAAS,KAAA,EAAO,SAAA;EAQhB,UAAA,CAAW,SAAA;EAQX,IAAA,CAAA,GAAQ,SAAA;EAIR,KAAA,CAAA;EAAA,QAIQ,IAAA;EAAA,QASA,IAAA;AAAA;;;;UC1DO,UAAA;EACf,KAAA,CACE,GAAA,UACA,OAAA;IACE,MAAA;IACA,OAAA,GAAU,MAAA;IACV,IAAA;EAAA,IAED,OAAA;AAAA;AAAA,UAGY,kBAAA;;EAEf,KAAA,GAAQ,SAAA;AAAA;AAAA,UAGO,WAAA;;EAEf,SAAA;EDjBA;ECmBA,UAAA;EDnBS;ECqBT,UAAA;AAAA;AAAA,UAGe,aAAA;EDpBP;ECsBR,UAAA;AAAA;;ADVF;;;;;;;;;;;iBCyBgB,iBAAA,CAAkB,GAAA,WAAc,UAAA;AAAA,UAiB/B,WAAA;EDlCC;ECoChB,IAAA,CAAK,SAAA,UAAmB,OAAA,GAAU,WAAA,GAAc,OAAA,CAAQ,UAAA;ED5BxD;EC8BA,MAAA,CAAO,SAAA,UAAmB,OAAA,EAAS,aAAA,GAAgB,OAAA;EDtBnD;;;;;;EC6BA,WAAA,CAAY,SAAA,UAAmB,OAAA,GAAU,WAAA,GAAc,OAAA;;EAEvD,IAAA,IAAQ,SAAA;;EAER,OAAA,IAAW,OAAA;IAAU,EAAA;IAAY,MAAA;EAAA;EAzEjC;EA2EA,KAAA;AAAA;AAAA,iBAGc,iBAAA,CACd,MAAA,EAAQ,UAAA,EACR,cAAA,GAAiB,kBAAA,GAChB,WAAA;;;UCrEc,eAAA;EFZC;EEchB,SAAA;EFZA;EEcA,OAAA,GAAU,WAAA;AAAA;AAAA,UAGK,oBAAA;EACf,MAAA,EAAQ,eAAA;EACR,MAAA,EAAQ,UAAA;AAAA;AAAA,UAGO,kBAAA;EACf,QAAA;EACA,MAAA,EAAQ,KAAA;IAAQ,SAAA;IAAmB,KAAA;EAAA;AAAA;AAAA,UAGpB,uBAAA;EFbqB;;;;;;EEoBpC,eAAA;AAAA;AAAA,UAGe,gBAAA;EFCf;EECA,KAAA,IAAS,OAAA,CAAQ,oBAAA;EFGjB;EEDA,OAAA,IAAW,OAAA,CAAQ,kBAAA;EFcX;EEZR,OAAA,IAAW,aAAA,CAAc,oBAAA;EFYb;EAAA,SEVH,MAAA;AAAA;;ADhDX;;;;;;iBC0DgB,sBAAA,CACd,WAAA,EAAa,IAAA,CAAK,WAAA,sBAClB,OAAA,EAAS,eAAA,IACT,OAAA,GAAU,uBAAA,GACT,gBAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
//#region src/store.ts
|
|
6
|
+
/**
|
|
7
|
+
* Lock Store — persistence layer for ADT lock handles
|
|
8
|
+
*
|
|
9
|
+
* Provides an interface for registering/deregistering lock entries
|
|
10
|
+
* and a file-based implementation that persists to `~/.adt/locks.json`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* File-based lock store.
|
|
14
|
+
*
|
|
15
|
+
* Persists lock entries to a JSON file (default: `~/.adt/locks.json`).
|
|
16
|
+
* Each entry is keyed by objectUri — re-locking the same URI overwrites.
|
|
17
|
+
* File permissions are 0o600 (owner read/write only).
|
|
18
|
+
*/
|
|
19
|
+
var FileLockStore = class {
|
|
20
|
+
filePath;
|
|
21
|
+
constructor(baseDir) {
|
|
22
|
+
this.filePath = join(baseDir ?? join(homedir(), ".adt"), "locks.json");
|
|
23
|
+
}
|
|
24
|
+
register(entry) {
|
|
25
|
+
const entries = this.load();
|
|
26
|
+
const idx = entries.findIndex((e) => e.objectUri === entry.objectUri);
|
|
27
|
+
if (idx >= 0) entries[idx] = entry;
|
|
28
|
+
else entries.push(entry);
|
|
29
|
+
this.save(entries);
|
|
30
|
+
}
|
|
31
|
+
deregister(objectUri) {
|
|
32
|
+
const entries = this.load();
|
|
33
|
+
const filtered = entries.filter((e) => e.objectUri !== objectUri);
|
|
34
|
+
if (filtered.length !== entries.length) this.save(filtered);
|
|
35
|
+
}
|
|
36
|
+
list() {
|
|
37
|
+
return this.load();
|
|
38
|
+
}
|
|
39
|
+
clear() {
|
|
40
|
+
this.save([]);
|
|
41
|
+
}
|
|
42
|
+
load() {
|
|
43
|
+
if (!existsSync(this.filePath)) return [];
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(this.filePath, "utf8"));
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
save(entries) {
|
|
51
|
+
const dir = dirname(this.filePath);
|
|
52
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
53
|
+
writeFileSync(this.filePath, JSON.stringify(entries, null, 2), { mode: 384 });
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/service.ts
|
|
59
|
+
/**
|
|
60
|
+
* Parse SAP lock response XML to extract lock handle and correlation info.
|
|
61
|
+
*
|
|
62
|
+
* Response format:
|
|
63
|
+
* ```xml
|
|
64
|
+
* <asx:abap>...<DATA>
|
|
65
|
+
* <LOCK_HANDLE>xxx</LOCK_HANDLE>
|
|
66
|
+
* <CORRNR>yyy</CORRNR>
|
|
67
|
+
* <CORRUSER>zzz</CORRUSER>
|
|
68
|
+
* </DATA>...</asx:abap>
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
function parseLockResponse(xml) {
|
|
72
|
+
const handleMatch = xml.match(/<LOCK_HANDLE>([^<]+)<\/LOCK_HANDLE>/);
|
|
73
|
+
if (!handleMatch) throw new Error("Failed to parse lock handle from SAP response. The response format may have changed.");
|
|
74
|
+
const corrNrMatch = xml.match(/<CORRNR>([^<]+)<\/CORRNR>/);
|
|
75
|
+
const corrUserMatch = xml.match(/<CORRUSER>([^<]+)<\/CORRUSER>/);
|
|
76
|
+
return {
|
|
77
|
+
handle: handleMatch[1],
|
|
78
|
+
correlationNumber: corrNrMatch?.[1],
|
|
79
|
+
correlationUser: corrUserMatch?.[1]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function createLockService(client, serviceOptions) {
|
|
83
|
+
const store = serviceOptions?.store;
|
|
84
|
+
/** Headers for lock operations — stateful session with security session reuse */
|
|
85
|
+
const lockHeaders = {
|
|
86
|
+
"X-sap-adt-sessiontype": "stateful",
|
|
87
|
+
"x-sap-security-session": "use",
|
|
88
|
+
Accept: "application/*,application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result"
|
|
89
|
+
};
|
|
90
|
+
/** Headers for unlock operations */
|
|
91
|
+
const unlockHeaders = {
|
|
92
|
+
"X-sap-adt-sessiontype": "stateful",
|
|
93
|
+
"x-sap-security-session": "use"
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
async lock(objectUri, options) {
|
|
97
|
+
const query = options?.transport ? `?_action=LOCK&accessMode=MODIFY&corrNr=${options.transport}` : "?_action=LOCK&accessMode=MODIFY";
|
|
98
|
+
const response = await client.fetch(`${objectUri}${query}`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: lockHeaders
|
|
101
|
+
});
|
|
102
|
+
const handle = parseLockResponse(typeof response === "string" ? response : response?.text ? await response.text() : JSON.stringify(response));
|
|
103
|
+
store?.register({
|
|
104
|
+
objectUri,
|
|
105
|
+
objectName: options?.objectName ?? objectUri.split("/").pop() ?? objectUri,
|
|
106
|
+
objectType: options?.objectType,
|
|
107
|
+
lockHandle: handle.handle,
|
|
108
|
+
transport: handle.correlationNumber,
|
|
109
|
+
lockedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
110
|
+
});
|
|
111
|
+
return handle;
|
|
112
|
+
},
|
|
113
|
+
async unlock(objectUri, options) {
|
|
114
|
+
const query = `?_action=UNLOCK&accessMode=MODIFY&lockHandle=${encodeURIComponent(options.lockHandle)}`;
|
|
115
|
+
await client.fetch(`${objectUri}${query}`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: unlockHeaders
|
|
118
|
+
});
|
|
119
|
+
store?.deregister(objectUri);
|
|
120
|
+
},
|
|
121
|
+
async forceUnlock(objectUri, options) {
|
|
122
|
+
const handle = await this.lock(objectUri, options);
|
|
123
|
+
await this.unlock(objectUri, { lockHandle: handle.handle });
|
|
124
|
+
},
|
|
125
|
+
list() {
|
|
126
|
+
return store?.list() ?? [];
|
|
127
|
+
},
|
|
128
|
+
async cleanup() {
|
|
129
|
+
const entries = store?.list() ?? [];
|
|
130
|
+
let ok = 0;
|
|
131
|
+
let failed = 0;
|
|
132
|
+
for (const entry of entries) try {
|
|
133
|
+
const query = `?_action=UNLOCK&accessMode=MODIFY&lockHandle=${encodeURIComponent(entry.lockHandle)}`;
|
|
134
|
+
await client.fetch(`${entry.objectUri}${query}`, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: unlockHeaders
|
|
137
|
+
});
|
|
138
|
+
store?.deregister(entry.objectUri);
|
|
139
|
+
ok++;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
142
|
+
if (msg.includes("not locked") || msg.includes("not enqueued")) {
|
|
143
|
+
store?.deregister(entry.objectUri);
|
|
144
|
+
ok++;
|
|
145
|
+
} else failed++;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
ok,
|
|
149
|
+
failed
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
clear() {
|
|
153
|
+
store?.clear();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/batch/batch-lock-session.ts
|
|
160
|
+
/**
|
|
161
|
+
* Create a batch lock session over a set of targets.
|
|
162
|
+
*
|
|
163
|
+
* The session does NOT auto-begin; callers drive the lifecycle explicitly so
|
|
164
|
+
* error handling stays visible at the call-site (`try { begin(); … } finally
|
|
165
|
+
* { release(); }`).
|
|
166
|
+
*/
|
|
167
|
+
function createBatchLockSession(lockService, targets, options) {
|
|
168
|
+
const rollback = options?.rollbackOnError ?? true;
|
|
169
|
+
const acquired = [];
|
|
170
|
+
let beganOnce = false;
|
|
171
|
+
let released = false;
|
|
172
|
+
return {
|
|
173
|
+
async begin() {
|
|
174
|
+
if (beganOnce) throw new Error("BatchLockSession.begin() called more than once");
|
|
175
|
+
beganOnce = true;
|
|
176
|
+
for (const target of targets) try {
|
|
177
|
+
const handle = await lockService.lock(target.objectUri, target.options);
|
|
178
|
+
acquired.push({
|
|
179
|
+
target,
|
|
180
|
+
handle
|
|
181
|
+
});
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (rollback) {
|
|
184
|
+
for (const a of acquired.splice(0).reverse()) try {
|
|
185
|
+
await lockService.unlock(a.target.objectUri, { lockHandle: a.handle.handle });
|
|
186
|
+
} catch {}
|
|
187
|
+
released = true;
|
|
188
|
+
}
|
|
189
|
+
throw err instanceof Error ? err : /* @__PURE__ */ new Error(`Batch lock failed: ${String(err)}`);
|
|
190
|
+
}
|
|
191
|
+
return [...acquired];
|
|
192
|
+
},
|
|
193
|
+
async release() {
|
|
194
|
+
if (released) return {
|
|
195
|
+
released: 0,
|
|
196
|
+
failed: []
|
|
197
|
+
};
|
|
198
|
+
released = true;
|
|
199
|
+
const failed = [];
|
|
200
|
+
let ok = 0;
|
|
201
|
+
for (const a of acquired.splice(0).reverse()) try {
|
|
202
|
+
await lockService.unlock(a.target.objectUri, { lockHandle: a.handle.handle });
|
|
203
|
+
ok++;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
failed.push({
|
|
206
|
+
objectUri: a.target.objectUri,
|
|
207
|
+
error: err instanceof Error ? err.message : String(err)
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
released: ok,
|
|
212
|
+
failed
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
handles() {
|
|
216
|
+
return [...acquired];
|
|
217
|
+
},
|
|
218
|
+
get active() {
|
|
219
|
+
return beganOnce && !released;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
export { FileLockStore, createBatchLockSession, createLockService, parseLockResponse };
|
|
226
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/store.ts","../src/service.ts","../src/batch/batch-lock-session.ts"],"sourcesContent":["/**\n * Lock Store — persistence layer for ADT lock handles\n *\n * Provides an interface for registering/deregistering lock entries\n * and a file-based implementation that persists to `~/.adt/locks.json`.\n */\n\nimport { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { homedir } from 'node:os';\nimport type { LockEntry } from './types';\n\n/**\n * Lock store interface — opt-in persistence for ADT lock handles.\n *\n * Implementations can be file-based, in-memory, or backed by any storage.\n */\nexport interface LockStore {\n /** Persist a lock entry after successful LOCK */\n register(entry: LockEntry): void;\n /** Remove a lock entry after successful UNLOCK */\n deregister(objectUri: string): void;\n /** List all persisted lock entries */\n list(): LockEntry[];\n /** Remove all entries */\n clear(): void;\n}\n\n/**\n * File-based lock store.\n *\n * Persists lock entries to a JSON file (default: `~/.adt/locks.json`).\n * Each entry is keyed by objectUri — re-locking the same URI overwrites.\n * File permissions are 0o600 (owner read/write only).\n */\nexport class FileLockStore implements LockStore {\n private readonly filePath: string;\n\n constructor(baseDir?: string) {\n const dir = baseDir ?? join(homedir(), '.adt');\n this.filePath = join(dir, 'locks.json');\n }\n\n register(entry: LockEntry): void {\n const entries = this.load();\n const idx = entries.findIndex((e) => e.objectUri === entry.objectUri);\n if (idx >= 0) entries[idx] = entry;\n else entries.push(entry);\n this.save(entries);\n }\n\n deregister(objectUri: string): void {\n const entries = this.load();\n const filtered = entries.filter((e) => e.objectUri !== objectUri);\n if (filtered.length !== entries.length) {\n this.save(filtered);\n }\n }\n\n list(): LockEntry[] {\n return this.load();\n }\n\n clear(): void {\n this.save([]);\n }\n\n private load(): LockEntry[] {\n if (!existsSync(this.filePath)) return [];\n try {\n return JSON.parse(readFileSync(this.filePath, 'utf8')) as LockEntry[];\n } catch {\n return [];\n }\n }\n\n private save(entries: LockEntry[]): void {\n const dir = dirname(this.filePath);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n writeFileSync(this.filePath, JSON.stringify(entries, null, 2), {\n mode: 0o600,\n });\n }\n}\n","/**\n * Lock Service — lock/unlock ADT objects via REST API\n *\n * Provides standalone lock/unlock operations that work with any\n * object URI. Optionally integrates with a LockStore for persistence.\n *\n * Usage:\n * import { createLockService, FileLockStore } from '@abapify/adt-locks';\n *\n * const locks = createLockService(client, { store: new FileLockStore() });\n * const handle = await locks.lock('/sap/bc/adt/oo/classes/zcl_test');\n * await locks.unlock('/sap/bc/adt/oo/classes/zcl_test', handle);\n */\n\nimport type { LockHandle, LockEntry } from './types';\nimport type { LockStore } from './store';\n\n/** Minimal client interface — anything with an authenticated fetch() */\nexport interface LockClient {\n fetch(\n url: string,\n options?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n },\n ): Promise<unknown>;\n}\n\nexport interface LockServiceOptions {\n /** Optional lock store for persistence */\n store?: LockStore;\n}\n\nexport interface LockOptions {\n /** Transport request */\n transport?: string;\n /** Object name (for store metadata) */\n objectName?: string;\n /** Object type (for store metadata) */\n objectType?: string;\n}\n\nexport interface UnlockOptions {\n /** Lock handle — REQUIRED. SAP always requires an explicit handle for unlock. */\n lockHandle: string;\n}\n\n/**\n * Parse SAP lock response XML to extract lock handle and correlation info.\n *\n * Response format:\n * ```xml\n * <asx:abap>...<DATA>\n * <LOCK_HANDLE>xxx</LOCK_HANDLE>\n * <CORRNR>yyy</CORRNR>\n * <CORRUSER>zzz</CORRUSER>\n * </DATA>...</asx:abap>\n * ```\n */\nexport function parseLockResponse(xml: string): LockHandle {\n const handleMatch = xml.match(/<LOCK_HANDLE>([^<]+)<\\/LOCK_HANDLE>/);\n if (!handleMatch) {\n throw new Error(\n 'Failed to parse lock handle from SAP response. The response format may have changed.',\n );\n }\n const corrNrMatch = xml.match(/<CORRNR>([^<]+)<\\/CORRNR>/);\n const corrUserMatch = xml.match(/<CORRUSER>([^<]+)<\\/CORRUSER>/);\n\n return {\n handle: handleMatch[1],\n correlationNumber: corrNrMatch?.[1],\n correlationUser: corrUserMatch?.[1],\n };\n}\n\nexport interface LockService {\n /** Acquire a lock on an ADT object */\n lock(objectUri: string, options?: LockOptions): Promise<LockHandle>;\n /** Release a lock on an ADT object (lock handle required) */\n unlock(objectUri: string, options: UnlockOptions): Promise<void>;\n /**\n * Force-unlock: acquire a lock to recover the handle, then immediately unlock.\n * Works when the object is locked by the current user but the handle was lost\n * (e.g., crashed session). If the object is locked by another user, the lock\n * call will fail with 403/409.\n */\n forceUnlock(objectUri: string, options?: LockOptions): Promise<void>;\n /** List all persisted lock entries (from store) */\n list(): LockEntry[];\n /** Try to UNLOCK every persisted entry, removing successful ones from store */\n cleanup(): Promise<{ ok: number; failed: number }>;\n /** Clear the store without sending any UNLOCK requests */\n clear(): void;\n}\n\nexport function createLockService(\n client: LockClient,\n serviceOptions?: LockServiceOptions,\n): LockService {\n const store = serviceOptions?.store;\n\n /** Headers for lock operations — stateful session with security session reuse */\n const lockHeaders = {\n 'X-sap-adt-sessiontype': 'stateful',\n 'x-sap-security-session': 'use',\n Accept:\n 'application/*,application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result',\n };\n\n /** Headers for unlock operations */\n const unlockHeaders = {\n 'X-sap-adt-sessiontype': 'stateful',\n 'x-sap-security-session': 'use',\n };\n\n return {\n async lock(objectUri, options) {\n const query = options?.transport\n ? `?_action=LOCK&accessMode=MODIFY&corrNr=${options.transport}`\n : '?_action=LOCK&accessMode=MODIFY';\n\n const response = await client.fetch(`${objectUri}${query}`, {\n method: 'POST',\n headers: lockHeaders,\n });\n\n // The adapter may return a string (text/xml) or a parsed object;\n // parseLockResponse needs the raw XML string.\n const body =\n typeof response === 'string'\n ? response\n : (response as Response)?.text\n ? await (response as Response).text()\n : JSON.stringify(response);\n const handle = parseLockResponse(body);\n\n store?.register({\n objectUri,\n objectName:\n options?.objectName ?? objectUri.split('/').pop() ?? objectUri,\n objectType: options?.objectType,\n lockHandle: handle.handle,\n transport: handle.correlationNumber,\n lockedAt: new Date().toISOString(),\n });\n\n return handle;\n },\n\n async unlock(objectUri, options) {\n const query = `?_action=UNLOCK&accessMode=MODIFY&lockHandle=${encodeURIComponent(options.lockHandle)}`;\n\n await client.fetch(`${objectUri}${query}`, {\n method: 'POST',\n headers: unlockHeaders,\n });\n\n store?.deregister(objectUri);\n },\n\n async forceUnlock(objectUri, options) {\n // Lock to recover/acquire the handle, then immediately unlock\n const handle = await this.lock(objectUri, options);\n await this.unlock(objectUri, { lockHandle: handle.handle });\n },\n\n list() {\n return store?.list() ?? [];\n },\n\n async cleanup() {\n const entries = store?.list() ?? [];\n let ok = 0;\n let failed = 0;\n\n for (const entry of entries) {\n try {\n const query = `?_action=UNLOCK&accessMode=MODIFY&lockHandle=${encodeURIComponent(entry.lockHandle)}`;\n await client.fetch(`${entry.objectUri}${query}`, {\n method: 'POST',\n headers: unlockHeaders,\n });\n store?.deregister(entry.objectUri);\n ok++;\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes('not locked') || msg.includes('not enqueued')) {\n store?.deregister(entry.objectUri);\n ok++;\n } else {\n failed++;\n }\n }\n }\n\n return { ok, failed };\n },\n\n clear() {\n store?.clear();\n },\n };\n}\n","/**\n * BatchLockSession — acquire and release N locks with best-effort rollback.\n *\n * SAP ADT has no transactional multi-object save. A \"batch lock\" is therefore\n * a **best-effort** construct:\n *\n * - `begin()` sequentially acquires a lock on every target. If any lock\n * acquisition fails, every previously acquired lock in this session is\n * released before the error is rethrown — leaving the backend in the\n * same state it was before `begin()` was called.\n *\n * - `release()` unlocks every successfully acquired handle, swallowing\n * per-object unlock failures (they are surfaced through the returned\n * result for the caller to inspect).\n *\n * - `handles()` exposes the acquired handles so callers can thread them\n * into downstream save/PUT flows without re-locking.\n *\n * The session is **single-use**: once released (explicitly or via rollback),\n * it cannot be reused. Tests exercise acquire/release/partial-failure paths.\n *\n * Consumer rationale: ADK's per-object `save({mode:'update'})` already owns\n * its own lock/unlock cycle. `BatchLockSession` is therefore positioned as an\n * outer pre-flight primitive — acquire across a whole ChangePlan to catch\n * conflicts BEFORE we start mutating SAP, release before handing off to ADK.\n * When ADK's save cycle later re-locks per object it simply re-uses the same\n * security session (the CSRF token survives), so the double lock is cheap.\n */\nimport type { LockHandle } from '../types';\nimport type { LockService, LockOptions } from '../service';\n\nexport interface BatchLockTarget {\n /** ADT object URI — e.g. `/sap/bc/adt/oo/classes/zcl_foo` */\n objectUri: string;\n /** Per-target lock options (transport, object name/type for the store). */\n options?: LockOptions;\n}\n\nexport interface BatchLockAcquisition {\n target: BatchLockTarget;\n handle: LockHandle;\n}\n\nexport interface BatchReleaseResult {\n released: number;\n failed: Array<{ objectUri: string; error: string }>;\n}\n\nexport interface BatchLockSessionOptions {\n /**\n * When true, `begin()` stops on the first failure and rolls back locks\n * already acquired in this session. When false (rare), errors are\n * collected and returned but already-acquired locks are kept — use only\n * for diagnostic tooling. Default: true.\n */\n rollbackOnError?: boolean;\n}\n\nexport interface BatchLockSession {\n /** Acquire all configured locks. Rollback on failure (by default). */\n begin(): Promise<BatchLockAcquisition[]>;\n /** Release every currently held handle. Safe to call multiple times. */\n release(): Promise<BatchReleaseResult>;\n /** Snapshot of currently held handles. */\n handles(): ReadonlyArray<BatchLockAcquisition>;\n /** Has `begin()` completed successfully? */\n readonly active: boolean;\n}\n\n/**\n * Create a batch lock session over a set of targets.\n *\n * The session does NOT auto-begin; callers drive the lifecycle explicitly so\n * error handling stays visible at the call-site (`try { begin(); … } finally\n * { release(); }`).\n */\nexport function createBatchLockSession(\n lockService: Pick<LockService, 'lock' | 'unlock'>,\n targets: BatchLockTarget[],\n options?: BatchLockSessionOptions,\n): BatchLockSession {\n const rollback = options?.rollbackOnError ?? true;\n const acquired: BatchLockAcquisition[] = [];\n let beganOnce = false;\n let released = false;\n\n return {\n async begin() {\n if (beganOnce) {\n throw new Error('BatchLockSession.begin() called more than once');\n }\n beganOnce = true;\n\n for (const target of targets) {\n try {\n const handle = await lockService.lock(\n target.objectUri,\n target.options,\n );\n acquired.push({ target, handle });\n } catch (err) {\n if (rollback) {\n // Best-effort: release what we already have, then propagate.\n for (const a of acquired.splice(0).reverse()) {\n try {\n await lockService.unlock(a.target.objectUri, {\n lockHandle: a.handle.handle,\n });\n } catch {\n /* swallow — primary failure is the real signal */\n }\n }\n // Session is done — no further release() work possible.\n released = true;\n }\n throw err instanceof Error\n ? err\n : new Error(`Batch lock failed: ${String(err)}`);\n }\n }\n return [...acquired];\n },\n\n async release() {\n if (released) return { released: 0, failed: [] };\n released = true;\n\n const failed: BatchReleaseResult['failed'] = [];\n let ok = 0;\n // Release in reverse acquisition order — symmetric with begin()'s\n // rollback path and avoids surprising the SAP enqueue server.\n for (const a of acquired.splice(0).reverse()) {\n try {\n await lockService.unlock(a.target.objectUri, {\n lockHandle: a.handle.handle,\n });\n ok++;\n } catch (err) {\n failed.push({\n objectUri: a.target.objectUri,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n return { released: ok, failed };\n },\n\n handles() {\n return [...acquired];\n },\n\n get active() {\n return beganOnce && !released;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmCA,IAAa,gBAAb,MAAgD;CAC9C,AAAiB;CAEjB,YAAY,SAAkB;AAE5B,OAAK,WAAW,KADJ,WAAW,KAAK,SAAS,EAAE,OAAO,EACpB,aAAa;;CAGzC,SAAS,OAAwB;EAC/B,MAAM,UAAU,KAAK,MAAM;EAC3B,MAAM,MAAM,QAAQ,WAAW,MAAM,EAAE,cAAc,MAAM,UAAU;AACrE,MAAI,OAAO,EAAG,SAAQ,OAAO;MACxB,SAAQ,KAAK,MAAM;AACxB,OAAK,KAAK,QAAQ;;CAGpB,WAAW,WAAyB;EAClC,MAAM,UAAU,KAAK,MAAM;EAC3B,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,cAAc,UAAU;AACjE,MAAI,SAAS,WAAW,QAAQ,OAC9B,MAAK,KAAK,SAAS;;CAIvB,OAAoB;AAClB,SAAO,KAAK,MAAM;;CAGpB,QAAc;AACZ,OAAK,KAAK,EAAE,CAAC;;CAGf,AAAQ,OAAoB;AAC1B,MAAI,CAAC,WAAW,KAAK,SAAS,CAAE,QAAO,EAAE;AACzC,MAAI;AACF,UAAO,KAAK,MAAM,aAAa,KAAK,UAAU,OAAO,CAAC;UAChD;AACN,UAAO,EAAE;;;CAIb,AAAQ,KAAK,SAA4B;EACvC,MAAM,MAAM,QAAQ,KAAK,SAAS;AAClC,MAAI,CAAC,WAAW,IAAI,CAAE,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;AACzD,gBAAc,KAAK,UAAU,KAAK,UAAU,SAAS,MAAM,EAAE,EAAE,EAC7D,MAAM,KACP,CAAC;;;;;;;;;;;;;;;;;;ACrBN,SAAgB,kBAAkB,KAAyB;CACzD,MAAM,cAAc,IAAI,MAAM,sCAAsC;AACpE,KAAI,CAAC,YACH,OAAM,IAAI,MACR,uFACD;CAEH,MAAM,cAAc,IAAI,MAAM,4BAA4B;CAC1D,MAAM,gBAAgB,IAAI,MAAM,gCAAgC;AAEhE,QAAO;EACL,QAAQ,YAAY;EACpB,mBAAmB,cAAc;EACjC,iBAAiB,gBAAgB;EAClC;;AAuBH,SAAgB,kBACd,QACA,gBACa;CACb,MAAM,QAAQ,gBAAgB;;CAG9B,MAAM,cAAc;EAClB,yBAAyB;EACzB,0BAA0B;EAC1B,QACE;EACH;;CAGD,MAAM,gBAAgB;EACpB,yBAAyB;EACzB,0BAA0B;EAC3B;AAED,QAAO;EACL,MAAM,KAAK,WAAW,SAAS;GAC7B,MAAM,QAAQ,SAAS,YACnB,0CAA0C,QAAQ,cAClD;GAEJ,MAAM,WAAW,MAAM,OAAO,MAAM,GAAG,YAAY,SAAS;IAC1D,QAAQ;IACR,SAAS;IACV,CAAC;GAUF,MAAM,SAAS,kBALb,OAAO,aAAa,WAChB,WACC,UAAuB,OACtB,MAAO,SAAsB,MAAM,GACnC,KAAK,UAAU,SAAS,CACM;AAEtC,UAAO,SAAS;IACd;IACA,YACE,SAAS,cAAc,UAAU,MAAM,IAAI,CAAC,KAAK,IAAI;IACvD,YAAY,SAAS;IACrB,YAAY,OAAO;IACnB,WAAW,OAAO;IAClB,2BAAU,IAAI,MAAM,EAAC,aAAa;IACnC,CAAC;AAEF,UAAO;;EAGT,MAAM,OAAO,WAAW,SAAS;GAC/B,MAAM,QAAQ,gDAAgD,mBAAmB,QAAQ,WAAW;AAEpG,SAAM,OAAO,MAAM,GAAG,YAAY,SAAS;IACzC,QAAQ;IACR,SAAS;IACV,CAAC;AAEF,UAAO,WAAW,UAAU;;EAG9B,MAAM,YAAY,WAAW,SAAS;GAEpC,MAAM,SAAS,MAAM,KAAK,KAAK,WAAW,QAAQ;AAClD,SAAM,KAAK,OAAO,WAAW,EAAE,YAAY,OAAO,QAAQ,CAAC;;EAG7D,OAAO;AACL,UAAO,OAAO,MAAM,IAAI,EAAE;;EAG5B,MAAM,UAAU;GACd,MAAM,UAAU,OAAO,MAAM,IAAI,EAAE;GACnC,IAAI,KAAK;GACT,IAAI,SAAS;AAEb,QAAK,MAAM,SAAS,QAClB,KAAI;IACF,MAAM,QAAQ,gDAAgD,mBAAmB,MAAM,WAAW;AAClG,UAAM,OAAO,MAAM,GAAG,MAAM,YAAY,SAAS;KAC/C,QAAQ;KACR,SAAS;KACV,CAAC;AACF,WAAO,WAAW,MAAM,UAAU;AAClC;YACO,KAAc;IACrB,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,QAAI,IAAI,SAAS,aAAa,IAAI,IAAI,SAAS,eAAe,EAAE;AAC9D,YAAO,WAAW,MAAM,UAAU;AAClC;UAEA;;AAKN,UAAO;IAAE;IAAI;IAAQ;;EAGvB,QAAQ;AACN,UAAO,OAAO;;EAEjB;;;;;;;;;;;;AC/HH,SAAgB,uBACd,aACA,SACA,SACkB;CAClB,MAAM,WAAW,SAAS,mBAAmB;CAC7C,MAAM,WAAmC,EAAE;CAC3C,IAAI,YAAY;CAChB,IAAI,WAAW;AAEf,QAAO;EACL,MAAM,QAAQ;AACZ,OAAI,UACF,OAAM,IAAI,MAAM,iDAAiD;AAEnE,eAAY;AAEZ,QAAK,MAAM,UAAU,QACnB,KAAI;IACF,MAAM,SAAS,MAAM,YAAY,KAC/B,OAAO,WACP,OAAO,QACR;AACD,aAAS,KAAK;KAAE;KAAQ;KAAQ,CAAC;YAC1B,KAAK;AACZ,QAAI,UAAU;AAEZ,UAAK,MAAM,KAAK,SAAS,OAAO,EAAE,CAAC,SAAS,CAC1C,KAAI;AACF,YAAM,YAAY,OAAO,EAAE,OAAO,WAAW,EAC3C,YAAY,EAAE,OAAO,QACtB,CAAC;aACI;AAKV,gBAAW;;AAEb,UAAM,eAAe,QACjB,sBACA,IAAI,MAAM,sBAAsB,OAAO,IAAI,GAAG;;AAGtD,UAAO,CAAC,GAAG,SAAS;;EAGtB,MAAM,UAAU;AACd,OAAI,SAAU,QAAO;IAAE,UAAU;IAAG,QAAQ,EAAE;IAAE;AAChD,cAAW;GAEX,MAAM,SAAuC,EAAE;GAC/C,IAAI,KAAK;AAGT,QAAK,MAAM,KAAK,SAAS,OAAO,EAAE,CAAC,SAAS,CAC1C,KAAI;AACF,UAAM,YAAY,OAAO,EAAE,OAAO,WAAW,EAC3C,YAAY,EAAE,OAAO,QACtB,CAAC;AACF;YACO,KAAK;AACZ,WAAO,KAAK;KACV,WAAW,EAAE,OAAO;KACpB,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;KACxD,CAAC;;AAGN,UAAO;IAAE,UAAU;IAAI;IAAQ;;EAGjC,UAAU;AACR,UAAO,CAAC,GAAG,SAAS;;EAGtB,IAAI,SAAS;AACX,UAAO,aAAa,CAAC;;EAExB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abapify/adt-locks",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "ADT lock/unlock operations and lock store management",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"./package.json": "./package.json"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@abapify/adt-client": "0.4.
|
|
13
|
+
"@abapify/adt-client": "0.4.1"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|