skedyul 1.7.6 → 1.7.8

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
+ 1785985393398
package/dist/cli/index.js CHANGED
@@ -4658,6 +4658,128 @@ 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 context = body.context;
4688
+ if (!context?.workplaceId || !context?.appInstallationId || !context?.appId) {
4689
+ return {
4690
+ status: 400,
4691
+ body: {
4692
+ success: false,
4693
+ error: "Missing context (workplaceId, appInstallationId, and appId required)"
4694
+ }
4695
+ };
4696
+ }
4697
+ const env = body.env ?? {};
4698
+ const log = {
4699
+ info: (message, meta) => {
4700
+ console.log(`[batch:${body.handle}] ${message}`, meta ?? "");
4701
+ },
4702
+ warn: (message, meta) => {
4703
+ console.warn(`[batch:${body.handle}] ${message}`, meta ?? "");
4704
+ },
4705
+ error: (message, meta) => {
4706
+ console.error(`[batch:${body.handle}] ${message}`, meta ?? "");
4707
+ }
4708
+ };
4709
+ const requestConfig = {
4710
+ baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
4711
+ apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
4712
+ };
4713
+ try {
4714
+ if (body.method === "setup") {
4715
+ if (!operation.setup) {
4716
+ return {
4717
+ status: 200,
4718
+ body: {
4719
+ success: true,
4720
+ result: { state: {}, total: void 0 }
4721
+ }
4722
+ };
4723
+ }
4724
+ const result = await runWithConfig(requestConfig, async () => {
4725
+ return operation.setup({
4726
+ workplaceId: context.workplaceId,
4727
+ appInstallationId: context.appInstallationId,
4728
+ appId: context.appId,
4729
+ input: body.input,
4730
+ env,
4731
+ log
4732
+ });
4733
+ });
4734
+ return {
4735
+ status: 200,
4736
+ body: {
4737
+ success: true,
4738
+ result
4739
+ }
4740
+ };
4741
+ }
4742
+ if (body.method === "iterate") {
4743
+ const result = await runWithConfig(requestConfig, async () => {
4744
+ return operation.iterate({
4745
+ workplaceId: context.workplaceId,
4746
+ appInstallationId: context.appInstallationId,
4747
+ appId: context.appId,
4748
+ input: body.input,
4749
+ state: body.state,
4750
+ page: body.page,
4751
+ cursor: body.cursor,
4752
+ limit: body.limit,
4753
+ env,
4754
+ log
4755
+ });
4756
+ });
4757
+ return {
4758
+ status: 200,
4759
+ body: {
4760
+ success: true,
4761
+ result
4762
+ }
4763
+ };
4764
+ }
4765
+ return {
4766
+ status: 400,
4767
+ body: {
4768
+ success: false,
4769
+ error: `Unknown method: ${body.method}`
4770
+ }
4771
+ };
4772
+ } catch (err) {
4773
+ log.error("Batch operation failed", { error: err });
4774
+ return {
4775
+ status: 500,
4776
+ body: {
4777
+ success: false,
4778
+ error: err instanceof Error ? err.message : String(err)
4779
+ }
4780
+ };
4781
+ }
4782
+ }
4661
4783
  function createNotFoundResponse() {
4662
4784
  return {
4663
4785
  status: 404,
@@ -4737,6 +4859,9 @@ async function routeRequest(req, ctx) {
4737
4859
  if (req.path === "/mcp/batch" && req.method === "POST") {
4738
4860
  return handleMcpBatchRoute(req, ctx);
4739
4861
  }
4862
+ if (req.path === "/batch-operation" && req.method === "POST") {
4863
+ return handleBatchOperationRoute(req, ctx);
4864
+ }
4740
4865
  return createNotFoundResponse();
4741
4866
  } catch (err) {
4742
4867
  return createErrorResponse(err);
@@ -4778,11 +4903,13 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
4778
4903
  const defaultHeaders = getDefaultHeaders(config.cors);
4779
4904
  const registry = config.tools;
4780
4905
  const webhookRegistry = config.webhooks;
4906
+ const batchOperationRegistry = config.batchOperations;
4781
4907
  const ctx = {
4782
4908
  config,
4783
4909
  tools,
4784
4910
  registry,
4785
4911
  webhookRegistry,
4912
+ batchOperationRegistry,
4786
4913
  callTool,
4787
4914
  state,
4788
4915
  mcpServer,
@@ -4897,12 +5024,14 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
4897
5024
  const defaultHeaders = getDefaultHeaders(config.cors);
4898
5025
  const registry = config.tools;
4899
5026
  const webhookRegistry = config.webhooks;
5027
+ const batchOperationRegistry = config.batchOperations;
4900
5028
  let hasLoggedStartup = false;
4901
5029
  const ctx = {
4902
5030
  config,
4903
5031
  tools,
4904
5032
  registry,
4905
5033
  webhookRegistry,
5034
+ batchOperationRegistry,
4906
5035
  callTool,
4907
5036
  state,
4908
5037
  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,38 @@ 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 apiKey = ctx.env.GLOFOX_API_KEY
187
+ * const { data, pagination } = await glofox.getMembers({
188
+ * apiKey,
189
+ * limit: ctx.limit,
190
+ * offset: ctx.cursor ?? 0,
191
+ * })
192
+ * return {
193
+ * items: data,
194
+ * pagination: {
195
+ * hasMore: pagination.hasMore,
196
+ * nextCursor: pagination.offset + pagination.limit,
197
+ * }
198
+ * }
199
+ * },
200
+ * maxConcurrent: 1,
201
+ * pageSize: 50,
202
+ * })
203
+ */
204
+ 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,152 @@
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
+ /**
59
+ * Runtime environment variables from the platform invoke
60
+ * (APP_INSTALL overrides + SKEDYUL_API_TOKEN / SKEDYUL_API_URL).
61
+ */
62
+ env: Record<string, string>;
63
+ /** Logger instance */
64
+ log: {
65
+ info: (message: string, meta?: Record<string, unknown>) => void;
66
+ warn: (message: string, meta?: Record<string, unknown>) => void;
67
+ error: (message: string, meta?: Record<string, unknown>) => void;
68
+ };
69
+ }
70
+ /**
71
+ * Type for batch operation setup function.
72
+ */
73
+ export type BatchOperationSetupFn = (ctx: BatchOperationContext) => Promise<BatchOperationSetupResult>;
74
+ /**
75
+ * Type for batch operation iterate function.
76
+ */
77
+ export type BatchOperationIterateFn = (ctx: BatchOperationContext & {
78
+ /** Page to fetch (1-indexed, for page-based pagination) */
79
+ page?: number;
80
+ /** Cursor from previous iterate (for cursor-based pagination) */
81
+ cursor?: string | number;
82
+ /** Items per page */
83
+ limit: number;
84
+ }) => Promise<BatchOperationIterateResult>;
85
+ /**
86
+ * Batch operation definition.
87
+ *
88
+ * @example
89
+ * export default defineBatchOperation({
90
+ * handle: 'import_members',
91
+ * label: 'Import Members',
92
+ * entity: 'member',
93
+ * setup: async (ctx) => {
94
+ * const count = await glofox.getMemberCount()
95
+ * return { total: count }
96
+ * },
97
+ * iterate: async (ctx) => {
98
+ * const apiKey = ctx.env.GLOFOX_API_KEY
99
+ * const { data, pagination } = await glofox.getMembers({
100
+ * apiKey,
101
+ * limit: ctx.limit,
102
+ * offset: ctx.cursor ?? 0,
103
+ * })
104
+ * return {
105
+ * items: data,
106
+ * pagination: {
107
+ * hasMore: pagination.hasMore,
108
+ * nextCursor: pagination.offset + pagination.limit,
109
+ * }
110
+ * }
111
+ * },
112
+ * maxConcurrent: 1,
113
+ * pageSize: 50,
114
+ * })
115
+ */
116
+ export interface BatchOperationDefinition {
117
+ /** Unique handle for this operation */
118
+ handle: string;
119
+ /** Human-readable label */
120
+ label: string;
121
+ /** Description of what this operation does */
122
+ description?: string;
123
+ /**
124
+ * Entity handle that items will be upserted to.
125
+ * The platform applies CRM mappings and calls upsertMany.
126
+ */
127
+ entity: string;
128
+ /**
129
+ * Setup function called once at the start of the operation.
130
+ * Use to initialize state, fetch total count, etc.
131
+ */
132
+ setup?: BatchOperationSetupFn;
133
+ /**
134
+ * Iterate function called for each page of data.
135
+ * Must return items and pagination info.
136
+ */
137
+ iterate: BatchOperationIterateFn;
138
+ /**
139
+ * Maximum concurrent operations per app installation.
140
+ * @default 1
141
+ */
142
+ maxConcurrent?: number;
143
+ /**
144
+ * Default page size (items per iterate call).
145
+ * @default 50
146
+ */
147
+ pageSize?: number;
148
+ /**
149
+ * Optional icon for the operation (Lucide icon name)
150
+ */
151
+ icon?: string;
152
+ }
@@ -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,128 @@ 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 context = body.context;
2018
+ if (!context?.workplaceId || !context?.appInstallationId || !context?.appId) {
2019
+ return {
2020
+ status: 400,
2021
+ body: {
2022
+ success: false,
2023
+ error: "Missing context (workplaceId, appInstallationId, and appId required)"
2024
+ }
2025
+ };
2026
+ }
2027
+ const env = body.env ?? {};
2028
+ const log = {
2029
+ info: (message, meta) => {
2030
+ console.log(`[batch:${body.handle}] ${message}`, meta ?? "");
2031
+ },
2032
+ warn: (message, meta) => {
2033
+ console.warn(`[batch:${body.handle}] ${message}`, meta ?? "");
2034
+ },
2035
+ error: (message, meta) => {
2036
+ console.error(`[batch:${body.handle}] ${message}`, meta ?? "");
2037
+ }
2038
+ };
2039
+ const requestConfig = {
2040
+ baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2041
+ apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2042
+ };
2043
+ try {
2044
+ if (body.method === "setup") {
2045
+ if (!operation.setup) {
2046
+ return {
2047
+ status: 200,
2048
+ body: {
2049
+ success: true,
2050
+ result: { state: {}, total: void 0 }
2051
+ }
2052
+ };
2053
+ }
2054
+ const result = await runWithConfig(requestConfig, async () => {
2055
+ return operation.setup({
2056
+ workplaceId: context.workplaceId,
2057
+ appInstallationId: context.appInstallationId,
2058
+ appId: context.appId,
2059
+ input: body.input,
2060
+ env,
2061
+ log
2062
+ });
2063
+ });
2064
+ return {
2065
+ status: 200,
2066
+ body: {
2067
+ success: true,
2068
+ result
2069
+ }
2070
+ };
2071
+ }
2072
+ if (body.method === "iterate") {
2073
+ const result = await runWithConfig(requestConfig, async () => {
2074
+ return operation.iterate({
2075
+ workplaceId: context.workplaceId,
2076
+ appInstallationId: context.appInstallationId,
2077
+ appId: context.appId,
2078
+ input: body.input,
2079
+ state: body.state,
2080
+ page: body.page,
2081
+ cursor: body.cursor,
2082
+ limit: body.limit,
2083
+ env,
2084
+ log
2085
+ });
2086
+ });
2087
+ return {
2088
+ status: 200,
2089
+ body: {
2090
+ success: true,
2091
+ result
2092
+ }
2093
+ };
2094
+ }
2095
+ return {
2096
+ status: 400,
2097
+ body: {
2098
+ success: false,
2099
+ error: `Unknown method: ${body.method}`
2100
+ }
2101
+ };
2102
+ } catch (err) {
2103
+ log.error("Batch operation failed", { error: err });
2104
+ return {
2105
+ status: 500,
2106
+ body: {
2107
+ success: false,
2108
+ error: err instanceof Error ? err.message : String(err)
2109
+ }
2110
+ };
2111
+ }
2112
+ }
1991
2113
  function createNotFoundResponse() {
1992
2114
  return {
1993
2115
  status: 404,
@@ -2067,6 +2189,9 @@ async function routeRequest(req, ctx) {
2067
2189
  if (req.path === "/mcp/batch" && req.method === "POST") {
2068
2190
  return handleMcpBatchRoute(req, ctx);
2069
2191
  }
2192
+ if (req.path === "/batch-operation" && req.method === "POST") {
2193
+ return handleBatchOperationRoute(req, ctx);
2194
+ }
2070
2195
  return createNotFoundResponse();
2071
2196
  } catch (err) {
2072
2197
  return createErrorResponse(err);
@@ -2108,11 +2233,13 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2108
2233
  const defaultHeaders = getDefaultHeaders(config.cors);
2109
2234
  const registry = config.tools;
2110
2235
  const webhookRegistry = config.webhooks;
2236
+ const batchOperationRegistry = config.batchOperations;
2111
2237
  const ctx = {
2112
2238
  config,
2113
2239
  tools,
2114
2240
  registry,
2115
2241
  webhookRegistry,
2242
+ batchOperationRegistry,
2116
2243
  callTool,
2117
2244
  state,
2118
2245
  mcpServer,
@@ -2227,12 +2354,14 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
2227
2354
  const defaultHeaders = getDefaultHeaders(config.cors);
2228
2355
  const registry = config.tools;
2229
2356
  const webhookRegistry = config.webhooks;
2357
+ const batchOperationRegistry = config.batchOperations;
2230
2358
  let hasLoggedStartup = false;
2231
2359
  const ctx = {
2232
2360
  config,
2233
2361
  tools,
2234
2362
  registry,
2235
2363
  webhookRegistry,
2364
+ batchOperationRegistry,
2236
2365
  callTool,
2237
2366
  state,
2238
2367
  mcpServer,