@open-mercato/enterprise 0.6.6-develop.6317.1.b3be10ab84 → 0.6.6-develop.6331.1.a33b8e99b7
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/record_locks/di.js +23 -1
- package/dist/modules/record_locks/di.js.map +2 -2
- package/dist/modules/record_locks/lib/crudMutationGuardService.js +48 -18
- package/dist/modules/record_locks/lib/crudMutationGuardService.js.map +2 -2
- package/dist/modules/record_locks/lib/recordLockService.js +16 -0
- package/dist/modules/record_locks/lib/recordLockService.js.map +2 -2
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js +74 -35
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js.map +3 -3
- package/package.json +5 -5
- package/src/modules/record_locks/di.ts +28 -2
- package/src/modules/record_locks/lib/crudMutationGuardService.ts +88 -17
- package/src/modules/record_locks/lib/recordLockService.ts +25 -0
- package/src/modules/record_locks/widgets/injection/record-locking/widget.client.tsx +119 -31
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:enterprise] found
|
|
1
|
+
[build:enterprise] found 325 entry points
|
|
2
2
|
[build:enterprise] built successfully
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { asFunction } from "awilix";
|
|
2
|
+
import { createOptimisticLockGuardService } from "@open-mercato/shared/lib/crud/optimistic-lock";
|
|
3
|
+
import { getAllOptimisticLockReaders } from "@open-mercato/shared/lib/crud/optimistic-lock-store";
|
|
4
|
+
import { createCommandOptimisticLockGuardService } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
2
5
|
import { createRecordLockService } from "./lib/recordLockService.js";
|
|
3
6
|
import { createRecordLockCrudMutationGuardService } from "./lib/crudMutationGuardService.js";
|
|
4
7
|
function register(container) {
|
|
@@ -11,8 +14,27 @@ function register(container) {
|
|
|
11
14
|
rbacService: rbacService ?? null
|
|
12
15
|
})
|
|
13
16
|
).scoped(),
|
|
17
|
+
// CRUD guard decorator: chains the OSS `updated_at` floor first (built here
|
|
18
|
+
// because this DI key overrides the platform default), then adds the
|
|
19
|
+
// record_locks enrichment. record_locks can only ADD a 409, never skip the
|
|
20
|
+
// floor (S1/H2). Spec: .ai/specs/enterprise/2026-06-09-record-locks-unified-coverage.md (Phase 0)
|
|
14
21
|
crudMutationGuardService: asFunction(
|
|
15
|
-
(recordLockService) => createRecordLockCrudMutationGuardService(
|
|
22
|
+
(recordLockService, em) => createRecordLockCrudMutationGuardService(
|
|
23
|
+
recordLockService,
|
|
24
|
+
createOptimisticLockGuardService({
|
|
25
|
+
getEm: () => em,
|
|
26
|
+
readers: getAllOptimisticLockReaders()
|
|
27
|
+
})
|
|
28
|
+
)
|
|
29
|
+
).scoped(),
|
|
30
|
+
// Command guard override: lock-backed `resolveExpected` derived from
|
|
31
|
+
// authoritative server state (never requiring a client lock token, H2),
|
|
32
|
+
// awaited by `enforceCommandOptimisticLockWithGuards`. The OSS floor still
|
|
33
|
+
// runs first inside that runner.
|
|
34
|
+
commandOptimisticLockGuardService: asFunction(
|
|
35
|
+
(recordLockService) => createCommandOptimisticLockGuardService({
|
|
36
|
+
resolveExpected: ({ expectedFromHeader, resourceKind }) => recordLockService.resolveExpectedVersion({ expectedFromHeader, resourceKind })
|
|
37
|
+
})
|
|
16
38
|
).scoped()
|
|
17
39
|
});
|
|
18
40
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/record_locks/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asFunction } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { createRecordLockService } from './lib/recordLockService'\nimport type { RecordLockService } from './lib/recordLockService'\nimport { createRecordLockCrudMutationGuardService } from './lib/crudMutationGuardService'\n\nexport function register(container: AppContainer) {\n container.register({\n recordLockService: asFunction((\n em: EntityManager,\n moduleConfigService?: ModuleConfigService | null,\n actionLogService?: ActionLogService | null,\n rbacService?: RbacService | null,\n ) =>\n createRecordLockService({\n em,\n moduleConfigService: moduleConfigService ?? null,\n actionLogService: actionLogService ?? null,\n rbacService: rbacService ?? null,\n }),\n ).scoped(),\n crudMutationGuardService: asFunction((recordLockService: RecordLockService) =>\n createRecordLockCrudMutationGuardService(recordLockService),\n ).scoped(),\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kBAAkB;AAM3B,SAAS,+BAA+B;AAExC,SAAS,gDAAgD;AAElD,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,mBAAmB;AAAA,MAAW,CAC5B,IACA,qBACA,kBACA,gBAEA,wBAAwB;AAAA,QACtB;AAAA,QACA,qBAAqB,uBAAuB;AAAA,QAC5C,kBAAkB,oBAAoB;AAAA,QACtC,aAAa,eAAe;AAAA,MAC9B,CAAC;AAAA,IACH,EAAE,OAAO;AAAA,
|
|
4
|
+
"sourcesContent": ["import { asFunction } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { createOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport { createCommandOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { createRecordLockService } from './lib/recordLockService'\nimport type { RecordLockService } from './lib/recordLockService'\nimport { createRecordLockCrudMutationGuardService } from './lib/crudMutationGuardService'\n\nexport function register(container: AppContainer) {\n container.register({\n recordLockService: asFunction((\n em: EntityManager,\n moduleConfigService?: ModuleConfigService | null,\n actionLogService?: ActionLogService | null,\n rbacService?: RbacService | null,\n ) =>\n createRecordLockService({\n em,\n moduleConfigService: moduleConfigService ?? null,\n actionLogService: actionLogService ?? null,\n rbacService: rbacService ?? null,\n }),\n ).scoped(),\n // CRUD guard decorator: chains the OSS `updated_at` floor first (built here\n // because this DI key overrides the platform default), then adds the\n // record_locks enrichment. record_locks can only ADD a 409, never skip the\n // floor (S1/H2). Spec: .ai/specs/enterprise/2026-06-09-record-locks-unified-coverage.md (Phase 0)\n crudMutationGuardService: asFunction((\n recordLockService: RecordLockService,\n em: EntityManager,\n ) =>\n createRecordLockCrudMutationGuardService(\n recordLockService,\n createOptimisticLockGuardService({\n getEm: () => em,\n readers: getAllOptimisticLockReaders(),\n }),\n ),\n ).scoped(),\n // Command guard override: lock-backed `resolveExpected` derived from\n // authoritative server state (never requiring a client lock token, H2),\n // awaited by `enforceCommandOptimisticLockWithGuards`. The OSS floor still\n // runs first inside that runner.\n commandOptimisticLockGuardService: asFunction((recordLockService: RecordLockService) =>\n createCommandOptimisticLockGuardService({\n resolveExpected: ({ expectedFromHeader, resourceKind }) =>\n recordLockService.resolveExpectedVersion({ expectedFromHeader, resourceKind }),\n }),\n ).scoped(),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB;AAM3B,SAAS,wCAAwC;AACjD,SAAS,mCAAmC;AAC5C,SAAS,+CAA+C;AACxD,SAAS,+BAA+B;AAExC,SAAS,gDAAgD;AAElD,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,mBAAmB;AAAA,MAAW,CAC5B,IACA,qBACA,kBACA,gBAEA,wBAAwB;AAAA,QACtB;AAAA,QACA,qBAAqB,uBAAuB;AAAA,QAC5C,kBAAkB,oBAAoB;AAAA,QACtC,aAAa,eAAe;AAAA,MAC9B,CAAC;AAAA,IACH,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKT,0BAA0B;AAAA,MAAW,CACnC,mBACA,OAEA;AAAA,QACE;AAAA,QACA,iCAAiC;AAAA,UAC/B,OAAO,MAAM;AAAA,UACb,SAAS,4BAA4B;AAAA,QACvC,CAAC;AAAA,MACH;AAAA,IACF,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKT,mCAAmC;AAAA,MAAW,CAAC,sBAC7C,wCAAwC;AAAA,QACtC,iBAAiB,CAAC,EAAE,oBAAoB,aAAa,MACnD,kBAAkB,uBAAuB,EAAE,oBAAoB,aAAa,CAAC;AAAA,MACjF,CAAC;AAAA,IACH,EAAE,OAAO;AAAA,EACX,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,36 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseOptimisticLockEnv
|
|
3
|
+
} from "@open-mercato/shared/lib/crud/optimistic-lock";
|
|
4
|
+
import { OPTIMISTIC_LOCK_ENV_VAR } from "@open-mercato/shared/lib/crud/optimistic-lock-headers";
|
|
1
5
|
import { readRecordLockHeaders } from "./recordLockService.js";
|
|
6
|
+
import { isRecordLockingEnabledForResource } from "./config.js";
|
|
2
7
|
function resolveRecordLockMutationMethod(operation) {
|
|
3
8
|
if (operation === "delete") return "DELETE";
|
|
4
9
|
return "PUT";
|
|
5
10
|
}
|
|
6
|
-
function
|
|
11
|
+
function resolveConfig(envValue) {
|
|
12
|
+
return parseOptimisticLockEnv(envValue !== void 0 ? envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR]);
|
|
13
|
+
}
|
|
14
|
+
function createRecordLockCrudMutationGuardService(recordLockService, ossFloorGuardService, options = {}) {
|
|
15
|
+
async function isRecordLockEnrichmentEnabled(resourceKind) {
|
|
16
|
+
try {
|
|
17
|
+
const settings = await recordLockService.getSettings();
|
|
18
|
+
return isRecordLockingEnabledForResource(settings, resourceKind);
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
return {
|
|
8
24
|
async validateMutation(input) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
const config = resolveConfig(options.envValue);
|
|
26
|
+
if (config.mode === "off") {
|
|
27
|
+
return { ok: true, shouldRunAfterSuccess: false, metadata: null };
|
|
28
|
+
}
|
|
29
|
+
const floorResult = await ossFloorGuardService.validateMutation(input);
|
|
30
|
+
if (!floorResult.ok) return floorResult;
|
|
31
|
+
if (!await isRecordLockEnrichmentEnabled(input.resourceKind)) {
|
|
32
|
+
return { ok: true, shouldRunAfterSuccess: false, metadata: null };
|
|
33
|
+
}
|
|
34
|
+
let enrichmentResult;
|
|
35
|
+
try {
|
|
36
|
+
enrichmentResult = await recordLockService.validateMutation({
|
|
37
|
+
tenantId: input.tenantId,
|
|
38
|
+
organizationId: input.organizationId ?? null,
|
|
39
|
+
userId: input.userId,
|
|
40
|
+
resourceKind: input.resourceKind,
|
|
41
|
+
resourceId: input.resourceId,
|
|
42
|
+
method: resolveRecordLockMutationMethod(input.operation),
|
|
43
|
+
headers: readRecordLockHeaders(input.requestHeaders),
|
|
44
|
+
mutationPayload: input.mutationPayload ?? null
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
return { ok: true, shouldRunAfterSuccess: false, metadata: null };
|
|
48
|
+
}
|
|
49
|
+
if (enrichmentResult.ok) {
|
|
20
50
|
return {
|
|
21
51
|
ok: true,
|
|
22
|
-
shouldRunAfterSuccess:
|
|
52
|
+
shouldRunAfterSuccess: enrichmentResult.resourceEnabled,
|
|
23
53
|
metadata: null
|
|
24
54
|
};
|
|
25
55
|
}
|
|
26
56
|
return {
|
|
27
57
|
ok: false,
|
|
28
|
-
status:
|
|
58
|
+
status: enrichmentResult.status,
|
|
29
59
|
body: {
|
|
30
|
-
error:
|
|
31
|
-
code:
|
|
32
|
-
lock:
|
|
33
|
-
conflict:
|
|
60
|
+
error: enrichmentResult.error,
|
|
61
|
+
code: enrichmentResult.code,
|
|
62
|
+
lock: enrichmentResult.lock ?? null,
|
|
63
|
+
conflict: enrichmentResult.conflict ?? null
|
|
34
64
|
}
|
|
35
65
|
};
|
|
36
66
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/record_locks/lib/crudMutationGuardService.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n CrudMutationGuardAfterSuccessInput,\n CrudMutationGuardValidateInput,\n CrudMutationGuardValidationResult,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { readRecordLockHeaders, type RecordLockService } from './recordLockService'\n\nexport type RecordLockCrudMutationGuardService = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction resolveRecordLockMutationMethod(operation: CrudMutationGuardValidateInput['operation']): 'PUT' | 'DELETE' {\n if (operation === 'delete') return 'DELETE'\n return 'PUT'\n}\n\nexport function createRecordLockCrudMutationGuardService(\n recordLockService: RecordLockService,\n): RecordLockCrudMutationGuardService {\n return {\n async validateMutation(input) {\n const
|
|
5
|
-
"mappings": "AAKA,SAAS,6BAAqD;
|
|
4
|
+
"sourcesContent": ["import type {\n CrudMutationGuardAfterSuccessInput,\n CrudMutationGuardValidateInput,\n CrudMutationGuardValidationResult,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport {\n parseOptimisticLockEnv,\n type OptimisticLockConfig,\n} from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { OPTIMISTIC_LOCK_ENV_VAR } from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\nimport { readRecordLockHeaders, type RecordLockService } from './recordLockService'\nimport { isRecordLockingEnabledForResource } from './config'\n\nexport type RecordLockCrudMutationGuardService = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\n/** The OSS floor service this decorator chains. Same shape as the platform default. */\nexport type OssCrudMutationGuardServiceLike = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction resolveRecordLockMutationMethod(operation: CrudMutationGuardValidateInput['operation']): 'PUT' | 'DELETE' {\n if (operation === 'delete') return 'DELETE'\n return 'PUT'\n}\n\nfunction resolveConfig(envValue: string | null | undefined): OptimisticLockConfig {\n return parseOptimisticLockEnv(envValue !== undefined ? envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR])\n}\n\nexport type CreateRecordLockCrudMutationGuardServiceOptions = {\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Enterprise CRUD mutation guard. A **decorator** over the OSS optimistic-lock\n * floor (S1/H2): it never replaces the floor, only adds blocks.\n *\n * Evaluation order (matches the spec's guard-layering chain):\n * 1. `OM_OPTIMISTIC_LOCK=off` \u2192 pure pass-through (single global kill switch;\n * neither floor nor enrichment runs).\n * 2. OSS `updated_at` floor runs first (delegated to the default OSS guard the\n * enterprise registration overrides). A stale write 409s here regardless of\n * any record-lock token / widget state \u2014 so a tokenless API/CLI client is\n * still caught (H1/H2).\n * 3. Only if the floor passes, AND record_locks is enabled for the resource in\n * settings, the enterprise `validateMutation` enrichment runs (pessimistic\n * lock + action-log diff). It can only ADD a 409, never relax the floor.\n *\n * Fail-closed (H3): if the enterprise enrichment throws, the decorator degrades\n * to the floor result (floor-pass \u21D2 allow), never to \"skip the guard\".\n */\nexport function createRecordLockCrudMutationGuardService(\n recordLockService: RecordLockService,\n ossFloorGuardService: OssCrudMutationGuardServiceLike,\n options: CreateRecordLockCrudMutationGuardServiceOptions = {},\n): RecordLockCrudMutationGuardService {\n async function isRecordLockEnrichmentEnabled(resourceKind: string): Promise<boolean> {\n try {\n const settings = await recordLockService.getSettings()\n return isRecordLockingEnabledForResource(settings, resourceKind)\n } catch {\n return false\n }\n }\n\n return {\n async validateMutation(input) {\n const config = resolveConfig(options.envValue)\n if (config.mode === 'off') {\n // Single global kill switch: neither floor nor enrichment runs.\n return { ok: true, shouldRunAfterSuccess: false, metadata: null }\n }\n\n // 1. OSS floor \u2014 always runs first, independent of any client lock token.\n const floorResult = await ossFloorGuardService.validateMutation(input)\n if (!floorResult.ok) return floorResult\n\n // 2. Enterprise enrichment \u2014 only when the resource is enabled in settings.\n if (!(await isRecordLockEnrichmentEnabled(input.resourceKind))) {\n return { ok: true, shouldRunAfterSuccess: false, metadata: null }\n }\n\n // 3. Fail-closed: a throwing enrichment degrades to the (passed) floor.\n let enrichmentResult: Awaited<ReturnType<RecordLockService['validateMutation']>>\n try {\n enrichmentResult = await recordLockService.validateMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n method: resolveRecordLockMutationMethod(input.operation),\n headers: readRecordLockHeaders(input.requestHeaders),\n mutationPayload: input.mutationPayload ?? null,\n })\n } catch {\n return { ok: true, shouldRunAfterSuccess: false, metadata: null }\n }\n\n if (enrichmentResult.ok) {\n return {\n ok: true,\n shouldRunAfterSuccess: enrichmentResult.resourceEnabled,\n metadata: null,\n }\n }\n\n return {\n ok: false,\n status: enrichmentResult.status,\n body: {\n error: enrichmentResult.error,\n code: enrichmentResult.code,\n lock: enrichmentResult.lock ?? null,\n conflict: enrichmentResult.conflict ?? null,\n },\n }\n },\n\n async afterMutationSuccess(input) {\n const method = resolveRecordLockMutationMethod(input.operation)\n const headers = readRecordLockHeaders(input.requestHeaders)\n\n await recordLockService.releaseAfterMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: headers.token,\n reason: 'saved',\n })\n\n await Promise.allSettled([\n recordLockService.emitIncomingChangesNotificationAfterMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n method,\n }),\n recordLockService.emitRecordDeletedNotificationAfterMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n method,\n }),\n ])\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -262,6 +262,22 @@ class RecordLockService {
|
|
|
262
262
|
await this.moduleConfigService.setValue(RECORD_LOCKS_MODULE_ID, RECORD_LOCKS_SETTINGS_NAME, settings);
|
|
263
263
|
return settings;
|
|
264
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Command-guard `resolveExpected` seam (Phase 0 / S1). Derives the expected
|
|
267
|
+
* version token the OSS command floor compares against the record's current
|
|
268
|
+
* server-side `updated_at`.
|
|
269
|
+
*
|
|
270
|
+
* The compare is server-authoritative: the command handler already loaded the
|
|
271
|
+
* record from the DB and passes its `updated_at` as `current`, so a stale
|
|
272
|
+
* write is detected from the version header alone — NEVER requiring a client
|
|
273
|
+
* record-lock token (H2). When record_locks is not enabled for the resource
|
|
274
|
+
* (settings off), this returns the header token unchanged so the floor still
|
|
275
|
+
* runs (pure floor behavior). The richer pessimistic/action-log conflict
|
|
276
|
+
* detection runs through `validateMutation` at the CRUD layer.
|
|
277
|
+
*/
|
|
278
|
+
async resolveExpectedVersion(input) {
|
|
279
|
+
return input.expectedFromHeader;
|
|
280
|
+
}
|
|
265
281
|
async acquire(input) {
|
|
266
282
|
this.scheduleCleanup(input.tenantId);
|
|
267
283
|
const settings = await this.getSettings();
|