skedyul 1.7.6 → 1.7.7

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 CHANGED
@@ -1 +1 @@
1
- 1785913911217
1
+ 1785972320346
package/dist/cli/index.js CHANGED
@@ -4658,6 +4658,107 @@ async function handleMcpBatchRoute(req, ctx) {
4658
4658
  }
4659
4659
  };
4660
4660
  }
4661
+ async function handleBatchOperationRoute(req, ctx) {
4662
+ const bodyResult = parseJsonBody(req);
4663
+ if (!bodyResult.success) {
4664
+ return bodyResult.error;
4665
+ }
4666
+ const body = bodyResult.data;
4667
+ const registry = ctx.batchOperationRegistry;
4668
+ if (!registry) {
4669
+ return {
4670
+ status: 404,
4671
+ body: {
4672
+ success: false,
4673
+ error: "No batch operations registered"
4674
+ }
4675
+ };
4676
+ }
4677
+ const operation = registry[body.handle];
4678
+ if (!operation) {
4679
+ return {
4680
+ status: 404,
4681
+ body: {
4682
+ success: false,
4683
+ error: `Batch operation not found: ${body.handle}`
4684
+ }
4685
+ };
4686
+ }
4687
+ const log = {
4688
+ info: (message, meta) => {
4689
+ console.log(`[batch:${body.handle}] ${message}`, meta ?? "");
4690
+ },
4691
+ warn: (message, meta) => {
4692
+ console.warn(`[batch:${body.handle}] ${message}`, meta ?? "");
4693
+ },
4694
+ error: (message, meta) => {
4695
+ console.error(`[batch:${body.handle}] ${message}`, meta ?? "");
4696
+ }
4697
+ };
4698
+ try {
4699
+ if (body.method === "setup") {
4700
+ if (!operation.setup) {
4701
+ return {
4702
+ status: 200,
4703
+ body: {
4704
+ success: true,
4705
+ result: { state: {}, total: void 0 }
4706
+ }
4707
+ };
4708
+ }
4709
+ const result = await operation.setup({
4710
+ workplaceId: body.workplaceId,
4711
+ appInstallationId: body.appInstallationId,
4712
+ appId: body.appId,
4713
+ input: body.input,
4714
+ log
4715
+ });
4716
+ return {
4717
+ status: 200,
4718
+ body: {
4719
+ success: true,
4720
+ result
4721
+ }
4722
+ };
4723
+ }
4724
+ if (body.method === "iterate") {
4725
+ const result = await operation.iterate({
4726
+ workplaceId: body.workplaceId,
4727
+ appInstallationId: body.appInstallationId,
4728
+ appId: body.appId,
4729
+ input: body.input,
4730
+ state: body.state,
4731
+ page: body.page,
4732
+ cursor: body.cursor,
4733
+ limit: body.limit,
4734
+ log
4735
+ });
4736
+ return {
4737
+ status: 200,
4738
+ body: {
4739
+ success: true,
4740
+ result
4741
+ }
4742
+ };
4743
+ }
4744
+ return {
4745
+ status: 400,
4746
+ body: {
4747
+ success: false,
4748
+ error: `Unknown method: ${body.method}`
4749
+ }
4750
+ };
4751
+ } catch (err) {
4752
+ log.error("Batch operation failed", { error: err });
4753
+ return {
4754
+ status: 500,
4755
+ body: {
4756
+ success: false,
4757
+ error: err instanceof Error ? err.message : String(err)
4758
+ }
4759
+ };
4760
+ }
4761
+ }
4661
4762
  function createNotFoundResponse() {
4662
4763
  return {
4663
4764
  status: 404,
@@ -4737,6 +4838,9 @@ async function routeRequest(req, ctx) {
4737
4838
  if (req.path === "/mcp/batch" && req.method === "POST") {
4738
4839
  return handleMcpBatchRoute(req, ctx);
4739
4840
  }
4841
+ if (req.path === "/batch-operation" && req.method === "POST") {
4842
+ return handleBatchOperationRoute(req, ctx);
4843
+ }
4740
4844
  return createNotFoundResponse();
4741
4845
  } catch (err) {
4742
4846
  return createErrorResponse(err);
@@ -4778,11 +4882,13 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
4778
4882
  const defaultHeaders = getDefaultHeaders(config.cors);
4779
4883
  const registry = config.tools;
4780
4884
  const webhookRegistry = config.webhooks;
4885
+ const batchOperationRegistry = config.batchOperations;
4781
4886
  const ctx = {
4782
4887
  config,
4783
4888
  tools,
4784
4889
  registry,
4785
4890
  webhookRegistry,
4891
+ batchOperationRegistry,
4786
4892
  callTool,
4787
4893
  state,
4788
4894
  mcpServer,
@@ -4897,12 +5003,14 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
4897
5003
  const defaultHeaders = getDefaultHeaders(config.cors);
4898
5004
  const registry = config.tools;
4899
5005
  const webhookRegistry = config.webhooks;
5006
+ const batchOperationRegistry = config.batchOperations;
4900
5007
  let hasLoggedStartup = false;
4901
5008
  const ctx = {
4902
5009
  config,
4903
5010
  tools,
4904
5011
  registry,
4905
5012
  webhookRegistry,
5013
+ batchOperationRegistry,
4906
5014
  callTool,
4907
5015
  state,
4908
5016
  mcpServer,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * This module defines the main configuration interfaces for Skedyul apps.
5
5
  */
6
- import type { ToolRegistry, WebhookRegistry, ToolMetadata, WebhookMetadata } from '../types';
6
+ import type { ToolRegistry, WebhookRegistry, ToolMetadata, WebhookMetadata, BatchOperationRegistry } from '../types';
7
7
  import type { ServerHooks } from '../types/handlers';
8
8
  import type { CoreApiConfig } from '../core/types';
9
9
  import type { EnvSchema, ComputeLayer, ModelDefinition, EntityDefinition, RelationshipDefinition, ChannelDefinition, WorkflowDefinition, PageDefinition, NavigationConfig, AgentDefinition, SignalDefinition, SetupStepDefinition } from './types';
@@ -103,6 +103,10 @@ export interface SkedyulConfig {
103
103
  webhooks?: WebhookRegistry | Promise<{
104
104
  webhookRegistry: WebhookRegistry;
105
105
  }>;
106
+ /** Batch operation registry - direct object or dynamic import */
107
+ batchOperations?: BatchOperationRegistry | Promise<{
108
+ batchOperationRegistry: BatchOperationRegistry;
109
+ }>;
106
110
  /** Lifecycle hooks for install, provision, uninstall, and oauth_callback */
107
111
  hooks?: ServerHooks;
108
112
  /** Provision configuration - direct object or dynamic import */
@@ -24,6 +24,7 @@ import type { AgentDefinition } from './types/agent';
24
24
  import type { EnvSchema } from './types/env';
25
25
  import type { NavigationConfig } from './types/navigation';
26
26
  import type { SetupStepDefinition } from './types/setup';
27
+ import type { BatchOperationDefinition } from './types/batch-operation';
27
28
  /**
28
29
  * Define a model with full type safety.
29
30
  *
@@ -166,3 +167,36 @@ export declare function defineNavigation(navigation: NavigationConfig): Navigati
166
167
  * })
167
168
  */
168
169
  export declare function defineSetupStep(step: SetupStepDefinition): SetupStepDefinition;
170
+ /**
171
+ * Define a batch operation with full type safety.
172
+ *
173
+ * Batch operations are long-running, paginated tasks like imports and syncs.
174
+ * The platform handles orchestration, progress tracking, CRM mapping, and upsert.
175
+ *
176
+ * @example
177
+ * export default defineBatchOperation({
178
+ * handle: 'import_members',
179
+ * label: 'Import Members',
180
+ * entity: 'member',
181
+ * setup: async (ctx) => {
182
+ * const count = await glofox.getMemberCount()
183
+ * return { total: count }
184
+ * },
185
+ * iterate: async (ctx) => {
186
+ * const { data, pagination } = await glofox.getMembers({
187
+ * limit: ctx.limit,
188
+ * offset: ctx.cursor ?? 0,
189
+ * })
190
+ * return {
191
+ * items: data,
192
+ * pagination: {
193
+ * hasMore: pagination.hasMore,
194
+ * nextCursor: pagination.offset + pagination.limit,
195
+ * }
196
+ * }
197
+ * },
198
+ * maxConcurrent: 1,
199
+ * pageSize: 50,
200
+ * })
201
+ */
202
+ export declare function defineBatchOperation(operation: BatchOperationDefinition): BatchOperationDefinition;
@@ -11,7 +11,7 @@
11
11
  export * from './types';
12
12
  export type { InstallConfig, ProvisionConfig, BuildConfig, CorsOptions, SkedyulConfig, SerializableSkedyulConfig, QueueScope, QueueConfig, SerializableQueueConfig, QueueRegistry, SequencerScope, SerializableSequencerConfig, SequencerRegistry, } from './app-config';
13
13
  export { defineConfig } from './app-config';
14
- export { defineModel, defineEntity, defineChannel, definePage, defineWorkflow, defineAgent, defineEnv, defineNavigation, defineSetupStep, } from './define';
14
+ export { defineModel, defineEntity, defineChannel, definePage, defineWorkflow, defineAgent, defineEnv, defineNavigation, defineSetupStep, defineBatchOperation, } from './define';
15
15
  export { SCHEMA_FILE_EXTENSIONS, loadSchema, saveSchema, serializeSchemaToJson, serializeSchemaToTypeScript, transformToBackendSchema, transformFromBackendSchema, type LoadSchemaOptions, type LoadSchemaResult, type SerializeSchemaOptions, type BackendModelDefinition, type BackendFieldDefinition, type BackendRelationshipDefinition, type BackendDesiredSchema, } from './schema-loader';
16
16
  export { loadAndResolveConfig, serializeResolvedConfig, type ResolvedConfig } from './resolver';
17
17
  export { getAllEnvKeys, getRequiredInstallEnvKeys } from './utils';
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Batch operation definition types.
3
+ *
4
+ * Batch operations allow apps to define long-running, paginated operations
5
+ * like member imports, data syncs, and batch API calls. The platform handles
6
+ * orchestration, progress tracking, CRM mapping, and upsert.
7
+ */
8
+ /**
9
+ * Pagination response from an iterate function.
10
+ * Supports both cursor-based and page-based pagination.
11
+ */
12
+ export interface BatchPagination {
13
+ /** Whether there are more items to fetch */
14
+ hasMore: boolean;
15
+ /** Current page number (1-indexed, for page-based pagination) */
16
+ page?: number;
17
+ /** Total number of pages (for page-based pagination) */
18
+ total?: number;
19
+ /** Cursor for next page (for cursor-based pagination) */
20
+ nextCursor?: string | number;
21
+ /** Items per page */
22
+ limit?: number;
23
+ }
24
+ /**
25
+ * Result from a batch operation setup function.
26
+ */
27
+ export interface BatchOperationSetupResult {
28
+ /** Optional state to persist across iterate calls */
29
+ state?: Record<string, unknown>;
30
+ /** Optional total count for progress display */
31
+ total?: number;
32
+ }
33
+ /**
34
+ * Result from a batch operation iterate function.
35
+ */
36
+ export interface BatchOperationIterateResult {
37
+ /** Array of raw items from the external source */
38
+ items: Record<string, unknown>[];
39
+ /** Pagination info for fetching more pages */
40
+ pagination: BatchPagination;
41
+ /** Optional state to persist for the next iterate call */
42
+ state?: Record<string, unknown>;
43
+ }
44
+ /**
45
+ * Context passed to batch operation setup and iterate functions.
46
+ */
47
+ export interface BatchOperationContext {
48
+ /** Workplace ID */
49
+ workplaceId: string;
50
+ /** App installation ID */
51
+ appInstallationId: string;
52
+ /** App ID */
53
+ appId: string;
54
+ /** User-provided input (if any) */
55
+ input?: Record<string, unknown>;
56
+ /** State from previous setup or iterate call */
57
+ state?: Record<string, unknown>;
58
+ /** Logger instance */
59
+ log: {
60
+ info: (message: string, meta?: Record<string, unknown>) => void;
61
+ warn: (message: string, meta?: Record<string, unknown>) => void;
62
+ error: (message: string, meta?: Record<string, unknown>) => void;
63
+ };
64
+ }
65
+ /**
66
+ * Type for batch operation setup function.
67
+ */
68
+ export type BatchOperationSetupFn = (ctx: BatchOperationContext) => Promise<BatchOperationSetupResult>;
69
+ /**
70
+ * Type for batch operation iterate function.
71
+ */
72
+ export type BatchOperationIterateFn = (ctx: BatchOperationContext & {
73
+ /** Page to fetch (1-indexed, for page-based pagination) */
74
+ page?: number;
75
+ /** Cursor from previous iterate (for cursor-based pagination) */
76
+ cursor?: string | number;
77
+ /** Items per page */
78
+ limit: number;
79
+ }) => Promise<BatchOperationIterateResult>;
80
+ /**
81
+ * Batch operation definition.
82
+ *
83
+ * @example
84
+ * export default defineBatchOperation({
85
+ * handle: 'import_members',
86
+ * label: 'Import Members',
87
+ * entity: 'member',
88
+ * setup: async (ctx) => {
89
+ * const count = await glofox.getMemberCount()
90
+ * return { total: count }
91
+ * },
92
+ * iterate: async (ctx) => {
93
+ * const { data, pagination } = await glofox.getMembers({
94
+ * limit: ctx.limit,
95
+ * offset: ctx.cursor ?? 0,
96
+ * })
97
+ * return {
98
+ * items: data,
99
+ * pagination: {
100
+ * hasMore: pagination.hasMore,
101
+ * nextCursor: pagination.offset + pagination.limit,
102
+ * }
103
+ * }
104
+ * },
105
+ * maxConcurrent: 1,
106
+ * pageSize: 50,
107
+ * })
108
+ */
109
+ export interface BatchOperationDefinition {
110
+ /** Unique handle for this operation */
111
+ handle: string;
112
+ /** Human-readable label */
113
+ label: string;
114
+ /** Description of what this operation does */
115
+ description?: string;
116
+ /**
117
+ * Entity handle that items will be upserted to.
118
+ * The platform applies CRM mappings and calls upsertMany.
119
+ */
120
+ entity: string;
121
+ /**
122
+ * Setup function called once at the start of the operation.
123
+ * Use to initialize state, fetch total count, etc.
124
+ */
125
+ setup?: BatchOperationSetupFn;
126
+ /**
127
+ * Iterate function called for each page of data.
128
+ * Must return items and pagination info.
129
+ */
130
+ iterate: BatchOperationIterateFn;
131
+ /**
132
+ * Maximum concurrent operations per app installation.
133
+ * @default 1
134
+ */
135
+ maxConcurrent?: number;
136
+ /**
137
+ * Default page size (items per iterate call).
138
+ * @default 50
139
+ */
140
+ pageSize?: number;
141
+ /**
142
+ * Optional icon for the operation (Lucide icon name)
143
+ */
144
+ icon?: string;
145
+ }
@@ -19,3 +19,4 @@ export * from './navigation';
19
19
  export * from './context';
20
20
  export * from './form';
21
21
  export * from './page';
22
+ export * from './batch-operation';
@@ -1988,6 +1988,107 @@ async function handleMcpBatchRoute(req, ctx) {
1988
1988
  }
1989
1989
  };
1990
1990
  }
1991
+ async function handleBatchOperationRoute(req, ctx) {
1992
+ const bodyResult = parseJsonBody(req);
1993
+ if (!bodyResult.success) {
1994
+ return bodyResult.error;
1995
+ }
1996
+ const body = bodyResult.data;
1997
+ const registry = ctx.batchOperationRegistry;
1998
+ if (!registry) {
1999
+ return {
2000
+ status: 404,
2001
+ body: {
2002
+ success: false,
2003
+ error: "No batch operations registered"
2004
+ }
2005
+ };
2006
+ }
2007
+ const operation = registry[body.handle];
2008
+ if (!operation) {
2009
+ return {
2010
+ status: 404,
2011
+ body: {
2012
+ success: false,
2013
+ error: `Batch operation not found: ${body.handle}`
2014
+ }
2015
+ };
2016
+ }
2017
+ const log = {
2018
+ info: (message, meta) => {
2019
+ console.log(`[batch:${body.handle}] ${message}`, meta ?? "");
2020
+ },
2021
+ warn: (message, meta) => {
2022
+ console.warn(`[batch:${body.handle}] ${message}`, meta ?? "");
2023
+ },
2024
+ error: (message, meta) => {
2025
+ console.error(`[batch:${body.handle}] ${message}`, meta ?? "");
2026
+ }
2027
+ };
2028
+ try {
2029
+ if (body.method === "setup") {
2030
+ if (!operation.setup) {
2031
+ return {
2032
+ status: 200,
2033
+ body: {
2034
+ success: true,
2035
+ result: { state: {}, total: void 0 }
2036
+ }
2037
+ };
2038
+ }
2039
+ const result = await operation.setup({
2040
+ workplaceId: body.workplaceId,
2041
+ appInstallationId: body.appInstallationId,
2042
+ appId: body.appId,
2043
+ input: body.input,
2044
+ log
2045
+ });
2046
+ return {
2047
+ status: 200,
2048
+ body: {
2049
+ success: true,
2050
+ result
2051
+ }
2052
+ };
2053
+ }
2054
+ if (body.method === "iterate") {
2055
+ const result = await operation.iterate({
2056
+ workplaceId: body.workplaceId,
2057
+ appInstallationId: body.appInstallationId,
2058
+ appId: body.appId,
2059
+ input: body.input,
2060
+ state: body.state,
2061
+ page: body.page,
2062
+ cursor: body.cursor,
2063
+ limit: body.limit,
2064
+ log
2065
+ });
2066
+ return {
2067
+ status: 200,
2068
+ body: {
2069
+ success: true,
2070
+ result
2071
+ }
2072
+ };
2073
+ }
2074
+ return {
2075
+ status: 400,
2076
+ body: {
2077
+ success: false,
2078
+ error: `Unknown method: ${body.method}`
2079
+ }
2080
+ };
2081
+ } catch (err) {
2082
+ log.error("Batch operation failed", { error: err });
2083
+ return {
2084
+ status: 500,
2085
+ body: {
2086
+ success: false,
2087
+ error: err instanceof Error ? err.message : String(err)
2088
+ }
2089
+ };
2090
+ }
2091
+ }
1991
2092
  function createNotFoundResponse() {
1992
2093
  return {
1993
2094
  status: 404,
@@ -2067,6 +2168,9 @@ async function routeRequest(req, ctx) {
2067
2168
  if (req.path === "/mcp/batch" && req.method === "POST") {
2068
2169
  return handleMcpBatchRoute(req, ctx);
2069
2170
  }
2171
+ if (req.path === "/batch-operation" && req.method === "POST") {
2172
+ return handleBatchOperationRoute(req, ctx);
2173
+ }
2070
2174
  return createNotFoundResponse();
2071
2175
  } catch (err) {
2072
2176
  return createErrorResponse(err);
@@ -2108,11 +2212,13 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2108
2212
  const defaultHeaders = getDefaultHeaders(config.cors);
2109
2213
  const registry = config.tools;
2110
2214
  const webhookRegistry = config.webhooks;
2215
+ const batchOperationRegistry = config.batchOperations;
2111
2216
  const ctx = {
2112
2217
  config,
2113
2218
  tools,
2114
2219
  registry,
2115
2220
  webhookRegistry,
2221
+ batchOperationRegistry,
2116
2222
  callTool,
2117
2223
  state,
2118
2224
  mcpServer,
@@ -2227,12 +2333,14 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
2227
2333
  const defaultHeaders = getDefaultHeaders(config.cors);
2228
2334
  const registry = config.tools;
2229
2335
  const webhookRegistry = config.webhooks;
2336
+ const batchOperationRegistry = config.batchOperations;
2230
2337
  let hasLoggedStartup = false;
2231
2338
  const ctx = {
2232
2339
  config,
2233
2340
  tools,
2234
2341
  registry,
2235
2342
  webhookRegistry,
2343
+ batchOperationRegistry,
2236
2344
  callTool,
2237
2345
  state,
2238
2346
  mcpServer,