skedyul 1.7.8 → 1.7.11
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/.build-stamp +1 -1
- package/dist/cli/index.js +164 -69
- package/dist/config/types/batch-operation.d.ts +77 -6
- package/dist/dedicated/server.js +164 -69
- package/dist/esm/index.mjs +185 -69
- package/dist/index.d.ts +1 -1
- package/dist/index.js +187 -69
- package/dist/schemas.d.ts +37 -0
- package/dist/server/route-handlers/handlers.d.ts +2 -2
- package/dist/server.js +164 -69
- package/dist/serverless/server.mjs +164 -69
- package/dist/types/batch-operation.d.ts +6 -3
- package/dist/types/index.d.ts +1 -1
- package/dist/types/invocation.d.ts +16 -1
- package/package.json +2 -2
package/dist/.build-stamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1786058725534
|
package/dist/cli/index.js
CHANGED
|
@@ -4658,6 +4658,39 @@ async function handleMcpBatchRoute(req, ctx) {
|
|
|
4658
4658
|
}
|
|
4659
4659
|
};
|
|
4660
4660
|
}
|
|
4661
|
+
function isBatchOperationFailure(result) {
|
|
4662
|
+
return result != null && typeof result === "object" && result.success === false && typeof result.error === "object" && result.error != null;
|
|
4663
|
+
}
|
|
4664
|
+
function toStringEnv(env) {
|
|
4665
|
+
const out = {};
|
|
4666
|
+
for (const [key, value] of Object.entries(env)) {
|
|
4667
|
+
if (value !== void 0) {
|
|
4668
|
+
out[key] = value;
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
return out;
|
|
4672
|
+
}
|
|
4673
|
+
function stripBillingFields(result) {
|
|
4674
|
+
const {
|
|
4675
|
+
billing: _billing,
|
|
4676
|
+
success: _success,
|
|
4677
|
+
error: _error,
|
|
4678
|
+
retry: _retry,
|
|
4679
|
+
...domain
|
|
4680
|
+
} = result;
|
|
4681
|
+
return domain;
|
|
4682
|
+
}
|
|
4683
|
+
function softFailureResponse(error, retry, billing) {
|
|
4684
|
+
return {
|
|
4685
|
+
status: 200,
|
|
4686
|
+
body: {
|
|
4687
|
+
success: false,
|
|
4688
|
+
error,
|
|
4689
|
+
...retry ? { retry } : {},
|
|
4690
|
+
billing: normalizeBilling(billing)
|
|
4691
|
+
}
|
|
4692
|
+
};
|
|
4693
|
+
}
|
|
4661
4694
|
async function handleBatchOperationRoute(req, ctx) {
|
|
4662
4695
|
const bodyResult = parseJsonBody(req);
|
|
4663
4696
|
if (!bodyResult.success) {
|
|
@@ -4666,49 +4699,80 @@ async function handleBatchOperationRoute(req, ctx) {
|
|
|
4666
4699
|
const body = bodyResult.data;
|
|
4667
4700
|
const registry = ctx.batchOperationRegistry;
|
|
4668
4701
|
if (!registry) {
|
|
4669
|
-
return {
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
}
|
|
4675
|
-
};
|
|
4702
|
+
return softFailureResponse({
|
|
4703
|
+
code: "NOT_FOUND",
|
|
4704
|
+
message: "No batch operations registered",
|
|
4705
|
+
category: "validation"
|
|
4706
|
+
});
|
|
4676
4707
|
}
|
|
4677
4708
|
const operation = registry[body.handle];
|
|
4678
4709
|
if (!operation) {
|
|
4679
|
-
return {
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
}
|
|
4685
|
-
};
|
|
4710
|
+
return softFailureResponse({
|
|
4711
|
+
code: "NOT_FOUND",
|
|
4712
|
+
message: `Batch operation not found: ${body.handle}`,
|
|
4713
|
+
category: "validation"
|
|
4714
|
+
});
|
|
4686
4715
|
}
|
|
4687
4716
|
const context = body.context;
|
|
4688
4717
|
if (!context?.workplaceId || !context?.appInstallationId || !context?.appId) {
|
|
4689
|
-
return {
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
}
|
|
4695
|
-
};
|
|
4718
|
+
return softFailureResponse({
|
|
4719
|
+
code: "VALIDATION_ERROR",
|
|
4720
|
+
message: "Missing context (workplaceId, appInstallationId, and appId required)",
|
|
4721
|
+
category: "validation"
|
|
4722
|
+
});
|
|
4696
4723
|
}
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4724
|
+
if (body.method !== "setup" && body.method !== "iterate") {
|
|
4725
|
+
return softFailureResponse({
|
|
4726
|
+
code: "VALIDATION_ERROR",
|
|
4727
|
+
message: `Unknown method: ${body.method}`,
|
|
4728
|
+
category: "validation"
|
|
4729
|
+
});
|
|
4730
|
+
}
|
|
4731
|
+
const requestEnv = body.env ?? {};
|
|
4732
|
+
const mergedEnv = toStringEnv(buildToolExecutionEnv(requestEnv));
|
|
4733
|
+
const originalEnv = { ...process.env };
|
|
4734
|
+
Object.assign(process.env, mergedEnv);
|
|
4735
|
+
const log = createContextLogger();
|
|
4736
|
+
const app = context.app ?? {
|
|
4737
|
+
id: context.appId,
|
|
4738
|
+
versionId: context.appId
|
|
4739
|
+
};
|
|
4740
|
+
const workplace = context.workplace ?? {
|
|
4741
|
+
id: context.workplaceId,
|
|
4742
|
+
subdomain: ""
|
|
4743
|
+
};
|
|
4744
|
+
const invocation = body.invocation ?? {
|
|
4745
|
+
invocationId: `${body.handle}-${body.method}-${Date.now()}`,
|
|
4746
|
+
invocationType: "batch_operation",
|
|
4747
|
+
batchOperationHandle: body.handle,
|
|
4748
|
+
batchOperationMethod: body.method,
|
|
4749
|
+
appInstallationId: context.appInstallationId
|
|
4708
4750
|
};
|
|
4709
4751
|
const requestConfig = {
|
|
4710
|
-
baseUrl:
|
|
4711
|
-
apiToken:
|
|
4752
|
+
baseUrl: mergedEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
|
|
4753
|
+
apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
|
|
4754
|
+
};
|
|
4755
|
+
const rateLimitContext = {
|
|
4756
|
+
app,
|
|
4757
|
+
appInstallationId: context.appInstallationId,
|
|
4758
|
+
invocation,
|
|
4759
|
+
preAcquiredLeases: parsePreAcquiredLeases(
|
|
4760
|
+
mergedEnv.SKEDYUL_RATE_LIMIT_LEASES
|
|
4761
|
+
),
|
|
4762
|
+
heldMutexQueueKeys: parseHeldMutexQueueKeys(
|
|
4763
|
+
mergedEnv.SKEDYUL_HELD_MUTEX_QUEUE_KEYS
|
|
4764
|
+
)
|
|
4765
|
+
};
|
|
4766
|
+
const opContextBase = {
|
|
4767
|
+
workplaceId: context.workplaceId,
|
|
4768
|
+
appInstallationId: context.appInstallationId,
|
|
4769
|
+
appId: context.appId,
|
|
4770
|
+
app,
|
|
4771
|
+
workplace,
|
|
4772
|
+
input: body.input,
|
|
4773
|
+
env: mergedEnv,
|
|
4774
|
+
invocation,
|
|
4775
|
+
log
|
|
4712
4776
|
};
|
|
4713
4777
|
try {
|
|
4714
4778
|
if (body.method === "setup") {
|
|
@@ -4717,67 +4781,98 @@ async function handleBatchOperationRoute(req, ctx) {
|
|
|
4717
4781
|
status: 200,
|
|
4718
4782
|
body: {
|
|
4719
4783
|
success: true,
|
|
4720
|
-
result: {
|
|
4784
|
+
result: {
|
|
4785
|
+
state: {},
|
|
4786
|
+
total: void 0,
|
|
4787
|
+
cascade: operation.cascade
|
|
4788
|
+
},
|
|
4789
|
+
billing: normalizeBilling(void 0)
|
|
4721
4790
|
}
|
|
4722
4791
|
};
|
|
4723
4792
|
}
|
|
4724
|
-
const
|
|
4725
|
-
return
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
input: body.input,
|
|
4730
|
-
env,
|
|
4731
|
-
log
|
|
4793
|
+
const raw2 = await runWithConfig(requestConfig, async () => {
|
|
4794
|
+
return runWithRateLimitExecutionContext(rateLimitContext, async () => {
|
|
4795
|
+
return runWithLogContext({ invocation }, async () => {
|
|
4796
|
+
return operation.setup(opContextBase);
|
|
4797
|
+
});
|
|
4732
4798
|
});
|
|
4733
4799
|
});
|
|
4800
|
+
if (isBatchOperationFailure(raw2)) {
|
|
4801
|
+
return softFailureResponse(raw2.error, raw2.retry, raw2.billing);
|
|
4802
|
+
}
|
|
4803
|
+
const setupResult = raw2;
|
|
4804
|
+
const domain2 = stripBillingFields(
|
|
4805
|
+
setupResult
|
|
4806
|
+
);
|
|
4734
4807
|
return {
|
|
4735
4808
|
status: 200,
|
|
4736
4809
|
body: {
|
|
4737
4810
|
success: true,
|
|
4738
|
-
result
|
|
4811
|
+
result: {
|
|
4812
|
+
...domain2,
|
|
4813
|
+
// Expose definition cascade to the platform workflow
|
|
4814
|
+
cascade: operation.cascade ?? domain2.cascade
|
|
4815
|
+
},
|
|
4816
|
+
billing: normalizeBilling(setupResult.billing)
|
|
4739
4817
|
}
|
|
4740
4818
|
};
|
|
4741
4819
|
}
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
log
|
|
4820
|
+
const iterateBody = body;
|
|
4821
|
+
const raw = await runWithConfig(requestConfig, async () => {
|
|
4822
|
+
return runWithRateLimitExecutionContext(rateLimitContext, async () => {
|
|
4823
|
+
return runWithLogContext({ invocation }, async () => {
|
|
4824
|
+
return operation.iterate({
|
|
4825
|
+
...opContextBase,
|
|
4826
|
+
state: iterateBody.state,
|
|
4827
|
+
page: iterateBody.page,
|
|
4828
|
+
cursor: iterateBody.cursor,
|
|
4829
|
+
limit: iterateBody.limit,
|
|
4830
|
+
cascadeEntities: iterateBody.cascadeEntities
|
|
4831
|
+
});
|
|
4755
4832
|
});
|
|
4756
4833
|
});
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
success: true,
|
|
4761
|
-
result
|
|
4762
|
-
}
|
|
4763
|
-
};
|
|
4834
|
+
});
|
|
4835
|
+
if (isBatchOperationFailure(raw)) {
|
|
4836
|
+
return softFailureResponse(raw.error, raw.retry, raw.billing);
|
|
4764
4837
|
}
|
|
4838
|
+
const iterateResult = raw;
|
|
4839
|
+
const domain = stripBillingFields(
|
|
4840
|
+
iterateResult
|
|
4841
|
+
);
|
|
4765
4842
|
return {
|
|
4766
|
-
status:
|
|
4843
|
+
status: 200,
|
|
4767
4844
|
body: {
|
|
4768
|
-
success:
|
|
4769
|
-
|
|
4845
|
+
success: true,
|
|
4846
|
+
result: domain,
|
|
4847
|
+
billing: normalizeBilling(iterateResult.billing)
|
|
4770
4848
|
}
|
|
4771
4849
|
};
|
|
4772
4850
|
} catch (err) {
|
|
4773
|
-
|
|
4851
|
+
if (err instanceof RateLimitExceededError) {
|
|
4852
|
+
return softFailureResponse(
|
|
4853
|
+
{
|
|
4854
|
+
code: "RATE_LIMITED",
|
|
4855
|
+
message: err.message,
|
|
4856
|
+
category: "external"
|
|
4857
|
+
},
|
|
4858
|
+
{ allowed: true, afterMs: err.retryAfterMs }
|
|
4859
|
+
);
|
|
4860
|
+
}
|
|
4861
|
+
log.error("Batch operation failed", err);
|
|
4774
4862
|
return {
|
|
4775
4863
|
status: 500,
|
|
4776
4864
|
body: {
|
|
4777
4865
|
success: false,
|
|
4778
|
-
error:
|
|
4866
|
+
error: {
|
|
4867
|
+
code: "INTERNAL_ERROR",
|
|
4868
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4869
|
+
category: "internal"
|
|
4870
|
+
},
|
|
4871
|
+
billing: normalizeBilling(void 0)
|
|
4779
4872
|
}
|
|
4780
4873
|
};
|
|
4874
|
+
} finally {
|
|
4875
|
+
process.env = originalEnv;
|
|
4781
4876
|
}
|
|
4782
4877
|
}
|
|
4783
4878
|
function createNotFoundResponse() {
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* like member imports, data syncs, and batch API calls. The platform handles
|
|
6
6
|
* orchestration, progress tracking, CRM mapping, and upsert.
|
|
7
7
|
*/
|
|
8
|
+
import type { InvocationContext } from '../../types/invocation';
|
|
9
|
+
import type { ToolBilling, ToolError, ToolRetry } from '../../types/tool';
|
|
10
|
+
import type { AppInfo, WorkplaceInfo } from '../../types/shared';
|
|
11
|
+
export type { ToolBilling, ToolError, ToolRetry };
|
|
8
12
|
/**
|
|
9
13
|
* Pagination response from an iterate function.
|
|
10
14
|
* Supports both cursor-based and page-based pagination.
|
|
@@ -21,6 +25,15 @@ export interface BatchPagination {
|
|
|
21
25
|
/** Items per page */
|
|
22
26
|
limit?: number;
|
|
23
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Soft-failure return from setup/iterate (tool-call shaped).
|
|
30
|
+
*/
|
|
31
|
+
export interface BatchOperationFailure {
|
|
32
|
+
success: false;
|
|
33
|
+
error: ToolError;
|
|
34
|
+
retry?: ToolRetry;
|
|
35
|
+
billing?: ToolBilling;
|
|
36
|
+
}
|
|
24
37
|
/**
|
|
25
38
|
* Result from a batch operation setup function.
|
|
26
39
|
*/
|
|
@@ -29,18 +42,59 @@ export interface BatchOperationSetupResult {
|
|
|
29
42
|
state?: Record<string, unknown>;
|
|
30
43
|
/** Optional total count for progress display */
|
|
31
44
|
total?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Cascade phases (usually injected from the operation definition by the
|
|
47
|
+
* route handler so the platform workflow can resolve map-gated targets).
|
|
48
|
+
*/
|
|
49
|
+
cascade?: BatchCascadePhase[];
|
|
50
|
+
/** Optional billing (normalized by the route if omitted) */
|
|
51
|
+
billing?: ToolBilling;
|
|
52
|
+
/** Optional success discriminator (default true when domain fields present) */
|
|
53
|
+
success?: true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Cascade phase for multi-entity batch imports.
|
|
57
|
+
* Platform upserts configured entities in `order` within each wave.
|
|
58
|
+
*/
|
|
59
|
+
export interface BatchCascadePhase {
|
|
60
|
+
/** App entity handle to upsert */
|
|
61
|
+
entity: string;
|
|
62
|
+
/** Lower runs first within the same wave */
|
|
63
|
+
order: number;
|
|
64
|
+
/** setup = once (typically first iterate); page = every iterate page */
|
|
65
|
+
wave: 'setup' | 'page';
|
|
66
|
+
/**
|
|
67
|
+
* Other cascade entities that must also be CRM-configured for this phase
|
|
68
|
+
* to run (e.g. plan requires package).
|
|
69
|
+
*/
|
|
70
|
+
requires?: string[];
|
|
32
71
|
}
|
|
33
72
|
/**
|
|
34
73
|
* Result from a batch operation iterate function.
|
|
35
74
|
*/
|
|
36
75
|
export interface BatchOperationIterateResult {
|
|
37
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Array of raw items for the primary `entity`.
|
|
78
|
+
* Prefer `itemsByEntity` for cascade imports; when both are set, platform
|
|
79
|
+
* uses `itemsByEntity` and falls back to `items` for the primary entity.
|
|
80
|
+
*/
|
|
38
81
|
items: Record<string, unknown>[];
|
|
82
|
+
/**
|
|
83
|
+
* Multi-entity items keyed by entity handle (cascade imports).
|
|
84
|
+
* Platform upserts each configured entity in cascade order.
|
|
85
|
+
*/
|
|
86
|
+
itemsByEntity?: Record<string, Record<string, unknown>[]>;
|
|
39
87
|
/** Pagination info for fetching more pages */
|
|
40
88
|
pagination: BatchPagination;
|
|
41
89
|
/** Optional state to persist for the next iterate call */
|
|
42
90
|
state?: Record<string, unknown>;
|
|
91
|
+
/** Optional billing (normalized by the route if omitted) */
|
|
92
|
+
billing?: ToolBilling;
|
|
93
|
+
/** Optional success discriminator (default true when domain fields present) */
|
|
94
|
+
success?: true;
|
|
43
95
|
}
|
|
96
|
+
export type BatchOperationSetupReturn = BatchOperationSetupResult | BatchOperationFailure;
|
|
97
|
+
export type BatchOperationIterateReturn = BatchOperationIterateResult | BatchOperationFailure;
|
|
44
98
|
/**
|
|
45
99
|
* Context passed to batch operation setup and iterate functions.
|
|
46
100
|
*/
|
|
@@ -51,15 +105,27 @@ export interface BatchOperationContext {
|
|
|
51
105
|
appInstallationId: string;
|
|
52
106
|
/** App ID */
|
|
53
107
|
appId: string;
|
|
108
|
+
/** App info with version (for rate-limit / SDK context) */
|
|
109
|
+
app?: AppInfo;
|
|
110
|
+
/** Workplace info when available */
|
|
111
|
+
workplace?: WorkplaceInfo;
|
|
54
112
|
/** User-provided input (if any) */
|
|
55
113
|
input?: Record<string, unknown>;
|
|
56
114
|
/** State from previous setup or iterate call */
|
|
57
115
|
state?: Record<string, unknown>;
|
|
58
116
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
117
|
+
* Merged environment (process.env + baked MCP_ENV + request env),
|
|
118
|
+
* same layering as tool handlers via buildToolExecutionEnv.
|
|
61
119
|
*/
|
|
62
120
|
env: Record<string, string>;
|
|
121
|
+
/** Invocation context for log traceability */
|
|
122
|
+
invocation?: InvocationContext;
|
|
123
|
+
/**
|
|
124
|
+
* Entity handles whose CRM maps are configured for this job's cascade.
|
|
125
|
+
* Platform injects this on iterate so apps can skip expensive fetches
|
|
126
|
+
* (e.g. per-member credits) when that entity is not mapped.
|
|
127
|
+
*/
|
|
128
|
+
cascadeEntities?: string[];
|
|
63
129
|
/** Logger instance */
|
|
64
130
|
log: {
|
|
65
131
|
info: (message: string, meta?: Record<string, unknown>) => void;
|
|
@@ -70,7 +136,7 @@ export interface BatchOperationContext {
|
|
|
70
136
|
/**
|
|
71
137
|
* Type for batch operation setup function.
|
|
72
138
|
*/
|
|
73
|
-
export type BatchOperationSetupFn = (ctx: BatchOperationContext) => Promise<
|
|
139
|
+
export type BatchOperationSetupFn = (ctx: BatchOperationContext) => Promise<BatchOperationSetupReturn>;
|
|
74
140
|
/**
|
|
75
141
|
* Type for batch operation iterate function.
|
|
76
142
|
*/
|
|
@@ -81,7 +147,7 @@ export type BatchOperationIterateFn = (ctx: BatchOperationContext & {
|
|
|
81
147
|
cursor?: string | number;
|
|
82
148
|
/** Items per page */
|
|
83
149
|
limit: number;
|
|
84
|
-
}) => Promise<
|
|
150
|
+
}) => Promise<BatchOperationIterateReturn>;
|
|
85
151
|
/**
|
|
86
152
|
* Batch operation definition.
|
|
87
153
|
*
|
|
@@ -121,10 +187,15 @@ export interface BatchOperationDefinition {
|
|
|
121
187
|
/** Description of what this operation does */
|
|
122
188
|
description?: string;
|
|
123
189
|
/**
|
|
124
|
-
*
|
|
190
|
+
* Primary entity handle (progress + start gate).
|
|
125
191
|
* The platform applies CRM mappings and calls upsertMany.
|
|
126
192
|
*/
|
|
127
193
|
entity: string;
|
|
194
|
+
/**
|
|
195
|
+
* Optional multi-entity cascade. When set, iterate may return
|
|
196
|
+
* `itemsByEntity` and the platform upserts map-configured phases in order.
|
|
197
|
+
*/
|
|
198
|
+
cascade?: BatchCascadePhase[];
|
|
128
199
|
/**
|
|
129
200
|
* Setup function called once at the start of the operation.
|
|
130
201
|
* Use to initialize state, fetch total count, etc.
|