alepha 0.19.2 → 0.19.4
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/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/dist/api/audits/index.d.ts +8 -8
- package/dist/api/invitations/index.d.ts +790 -0
- package/dist/api/invitations/index.d.ts.map +1 -0
- package/dist/api/invitations/index.js +665 -0
- package/dist/api/invitations/index.js.map +1 -0
- package/dist/api/jobs/index.browser.js +8 -9
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +90 -34
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +267 -44
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.browser.js +0 -1
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +3 -3
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +0 -1
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.browser.js +112 -1
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +90 -3
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +79 -12
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/{billing → api/payments}/index.d.ts +67 -49
- package/dist/api/payments/index.d.ts.map +1 -0
- package/dist/{billing → api/payments}/index.js +108 -74
- package/dist/api/payments/index.js.map +1 -0
- package/dist/api/subscriptions/index.d.ts +1692 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1870 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +27 -21
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +167 -34
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/workflows/index.browser.js +246 -0
- package/dist/api/workflows/index.browser.js.map +1 -0
- package/dist/api/workflows/index.d.ts +1618 -0
- package/dist/api/workflows/index.d.ts.map +1 -0
- package/dist/api/workflows/index.js +1504 -0
- package/dist/api/workflows/index.js.map +1 -0
- package/dist/cli/config/index.d.ts +6 -28
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +5 -10
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +11669 -208
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +60 -69
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +5 -0
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js +4 -0
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +69 -64
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +6 -2
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +38 -10
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +85 -26
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/core/index.browser.js +21 -2
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +33 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +25 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +25 -2
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +25 -2
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.js +24 -8
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +0 -18
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +25 -73
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +10 -32
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +25 -73
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +3 -3
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +2 -1
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +3 -3
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/router/index.browser.js +25 -3
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +16 -1
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +25 -3
- package/dist/react/router/index.js.map +1 -1
- package/dist/security/index.d.ts +28 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +28 -0
- package/dist/security/index.js.map +1 -1
- package/package.json +37 -20
- package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
- package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
- package/src/api/invitations/controllers/InvitationController.ts +84 -0
- package/src/api/invitations/entities/invitations.ts +33 -0
- package/src/api/invitations/index.ts +65 -0
- package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
- package/src/api/invitations/providers/InvitationProvider.ts +45 -0
- package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
- package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
- package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
- package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
- package/src/api/invitations/services/InvitationService.ts +556 -0
- package/src/api/jobs/__tests__/$job.spec.ts +876 -0
- package/src/api/jobs/controllers/AdminJobController.ts +44 -0
- package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
- package/src/api/jobs/index.ts +0 -3
- package/src/api/jobs/primitives/$job.ts +22 -11
- package/src/api/jobs/providers/JobProvider.ts +239 -25
- package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
- package/src/api/jobs/services/JobService.ts +51 -12
- package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
- package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
- package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
- package/src/api/parameters/index.browser.ts +12 -0
- package/src/api/parameters/primitives/$parameter.ts +20 -3
- package/src/api/parameters/services/ParameterProvider.ts +48 -7
- package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
- package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
- package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
- package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
- package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
- package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
- package/src/{billing → api/payments}/index.ts +31 -25
- package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
- package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
- package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
- package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +144 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
- package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
- package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
- package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
- package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
- package/src/api/users/__tests__/SessionService.spec.ts +142 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
- package/src/api/users/controllers/UserController.ts +3 -8
- package/src/api/users/notifications/UserNotifications.ts +23 -0
- package/src/api/users/schemas/loginSchema.ts +1 -1
- package/src/api/users/services/CredentialService.ts +51 -4
- package/src/api/users/services/RegistrationService.ts +38 -9
- package/src/api/users/services/SessionService.ts +62 -9
- package/src/api/users/services/UserService.ts +21 -12
- package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
- package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
- package/src/api/workflows/entities/workflowExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
- package/src/api/workflows/index.browser.ts +22 -0
- package/src/api/workflows/index.ts +124 -0
- package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
- package/src/api/workflows/primitives/$workflow.ts +202 -0
- package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
- package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
- package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
- package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
- package/src/api/workflows/services/WorkflowService.ts +382 -0
- package/src/cli/config/defineConfig.ts +17 -46
- package/src/cli/core/providers/ViteDevServerProvider.ts +45 -3
- package/src/cli/core/services/PackageManagerUtils.ts +3 -1
- package/src/cli/core/services/ProjectScaffolder.ts +5 -5
- package/src/cli/core/templates/agentMd.ts +14 -5
- package/src/cli/core/templates/webAppRouterTs.ts +5 -58
- package/src/cli/devtools/index.ts +21 -1
- package/src/cli/platform/index.ts +23 -2
- package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
- package/src/cli/vendor/index.ts +20 -3
- package/src/cli/vendor/services/VendorService.ts +126 -27
- package/src/core/Alepha.ts +10 -0
- package/src/core/__tests__/TypeProvider.spec.ts +4 -2
- package/src/core/providers/SchemaValidator.ts +1 -1
- package/src/core/providers/TypeProvider.ts +46 -3
- package/src/logger/index.ts +6 -1
- package/src/orm/__tests__/enums.spec.ts +22 -29
- package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
- package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
- package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
- package/src/orm/core/providers/DrizzleKitProvider.ts +56 -105
- package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
- package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
- package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
- package/src/security/primitives/$secure.ts +28 -0
- package/tsconfig.base.json +0 -1
- package/dist/billing/index.d.ts.map +0 -1
- package/dist/billing/index.js.map +0 -1
- package/src/billing/__tests__/BillingService.spec.ts +0 -136
- /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
- /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
- /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
|
@@ -49,7 +49,6 @@ export class UserService {
|
|
|
49
49
|
email: string,
|
|
50
50
|
userRealmName?: string,
|
|
51
51
|
method: "code" | "link" = "code",
|
|
52
|
-
verifyUrl?: string,
|
|
53
52
|
): Promise<boolean> {
|
|
54
53
|
this.log.trace("Requesting email verification", {
|
|
55
54
|
email,
|
|
@@ -84,12 +83,15 @@ export class UserService {
|
|
|
84
83
|
});
|
|
85
84
|
|
|
86
85
|
if (method === "link") {
|
|
87
|
-
// Build verification URL
|
|
88
|
-
const
|
|
86
|
+
// Build verification URL from realm settings (server-controlled, not user input)
|
|
87
|
+
const realm = this.realmProvider.getRealm(userRealmName);
|
|
88
|
+
const realmSettings = await realm.getSettings();
|
|
89
|
+
const baseUrl = realmSettings.verifyEmailUrl ?? "/verify-email";
|
|
90
|
+
const url = new URL(baseUrl, "http://localhost");
|
|
89
91
|
url.searchParams.set("email", email);
|
|
90
92
|
url.searchParams.set("token", verification.token);
|
|
91
|
-
const fullVerifyUrl =
|
|
92
|
-
? `${
|
|
93
|
+
const fullVerifyUrl = realmSettings.verifyEmailUrl
|
|
94
|
+
? `${baseUrl}${url.search}`
|
|
93
95
|
: url.pathname + url.search;
|
|
94
96
|
|
|
95
97
|
await this.userNotifications(userRealmName)?.emailVerificationLink.push(
|
|
@@ -270,13 +272,12 @@ export class UserService {
|
|
|
270
272
|
});
|
|
271
273
|
|
|
272
274
|
const realm = this.realmProvider.getRealm(userRealmName);
|
|
275
|
+
const realmSettings = await realm.getSettings();
|
|
273
276
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
// Check for existing user based on provided unique fields
|
|
277
|
+
// Check for existing user based on provided unique fields (scoped to realm)
|
|
277
278
|
if (data.username) {
|
|
278
279
|
const existingUser = await this.users(userRealmName).findOne({
|
|
279
|
-
where: { username: { ilike: data.username } },
|
|
280
|
+
where: { realm: realm.name, username: { ilike: data.username } },
|
|
280
281
|
});
|
|
281
282
|
|
|
282
283
|
if (existingUser) {
|
|
@@ -287,7 +288,7 @@ export class UserService {
|
|
|
287
288
|
|
|
288
289
|
if (data.email) {
|
|
289
290
|
const existingUser = await this.users(userRealmName).findOne({
|
|
290
|
-
where: { email: { eq: data.email } },
|
|
291
|
+
where: { realm: realm.name, email: { eq: data.email } },
|
|
291
292
|
});
|
|
292
293
|
|
|
293
294
|
if (existingUser) {
|
|
@@ -298,7 +299,7 @@ export class UserService {
|
|
|
298
299
|
|
|
299
300
|
if (data.phoneNumber) {
|
|
300
301
|
const existingUser = await this.users(userRealmName).findOne({
|
|
301
|
-
where: { phoneNumber: { eq: data.phoneNumber } },
|
|
302
|
+
where: { realm: realm.name, phoneNumber: { eq: data.phoneNumber } },
|
|
302
303
|
});
|
|
303
304
|
|
|
304
305
|
if (existingUser) {
|
|
@@ -311,7 +312,7 @@ export class UserService {
|
|
|
311
312
|
|
|
312
313
|
const user = await this.users(userRealmName).create({
|
|
313
314
|
...data,
|
|
314
|
-
roles: data.roles ??
|
|
315
|
+
roles: data.roles ?? realmSettings.defaultRoles,
|
|
315
316
|
realm: realm.name,
|
|
316
317
|
});
|
|
317
318
|
|
|
@@ -386,6 +387,14 @@ export class UserService {
|
|
|
386
387
|
this.log.trace("Deleting user", { id, userRealmName });
|
|
387
388
|
const user = await this.getUserById(id, userRealmName);
|
|
388
389
|
|
|
390
|
+
// Clean up related sessions and identities before deleting the user
|
|
391
|
+
await this.realmProvider
|
|
392
|
+
.sessionRepository(userRealmName)
|
|
393
|
+
.deleteMany({ userId: { eq: id } });
|
|
394
|
+
await this.realmProvider
|
|
395
|
+
.identityRepository(userRealmName)
|
|
396
|
+
.deleteMany({ userId: { eq: id } });
|
|
397
|
+
|
|
389
398
|
await this.users(userRealmName).deleteById(id);
|
|
390
399
|
this.log.info("User deleted", { userId: id });
|
|
391
400
|
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { Alepha, t } from "alepha";
|
|
2
|
+
import { $repository } from "alepha/orm";
|
|
3
|
+
import { AlephaOrmPostgres } from "alepha/orm/postgres";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
$workflow,
|
|
7
|
+
AlephaApiWorkflows,
|
|
8
|
+
workflowExecutions,
|
|
9
|
+
workflowStepExecutions,
|
|
10
|
+
} from "../index.ts";
|
|
11
|
+
|
|
12
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
describe("$workflow", () => {
|
|
15
|
+
// ----- Basic functionality -----
|
|
16
|
+
|
|
17
|
+
describe("basic functionality", () => {
|
|
18
|
+
it("should start and complete a single-step workflow", async () => {
|
|
19
|
+
const handler = vi.fn();
|
|
20
|
+
|
|
21
|
+
class App {
|
|
22
|
+
repo = $repository(workflowExecutions);
|
|
23
|
+
stepRepo = $repository(workflowStepExecutions);
|
|
24
|
+
myWorkflow = $workflow({
|
|
25
|
+
schema: t.object({ orderId: t.uuid() }),
|
|
26
|
+
steps: [
|
|
27
|
+
{
|
|
28
|
+
name: "processOrder",
|
|
29
|
+
handler: async ({ payload }) => {
|
|
30
|
+
handler(payload);
|
|
31
|
+
return { processed: true };
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
39
|
+
alepha.with(AlephaApiWorkflows);
|
|
40
|
+
alepha.with(App);
|
|
41
|
+
await alepha.start();
|
|
42
|
+
|
|
43
|
+
const app = alepha.inject(App);
|
|
44
|
+
const orderId = crypto.randomUUID();
|
|
45
|
+
const executionId = await app.myWorkflow.start({ orderId });
|
|
46
|
+
|
|
47
|
+
await vi.waitFor(async () => {
|
|
48
|
+
const exec = await app.repo.findById(executionId);
|
|
49
|
+
expect(exec?.status).toBe("completed");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(handler).toHaveBeenCalledWith(
|
|
54
|
+
expect.objectContaining({ orderId }),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Verify step is completed with result
|
|
58
|
+
const steps = await app.stepRepo.findMany({
|
|
59
|
+
where: { workflowExecutionId: { eq: executionId } },
|
|
60
|
+
});
|
|
61
|
+
expect(steps).toHaveLength(1);
|
|
62
|
+
expect(steps[0].status).toBe("completed");
|
|
63
|
+
expect(steps[0].result).toEqual({ processed: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should execute multi-step workflow in order", async () => {
|
|
67
|
+
const order: string[] = [];
|
|
68
|
+
|
|
69
|
+
class App {
|
|
70
|
+
repo = $repository(workflowExecutions);
|
|
71
|
+
myWorkflow = $workflow({
|
|
72
|
+
schema: t.object({ id: t.text() }),
|
|
73
|
+
steps: [
|
|
74
|
+
{
|
|
75
|
+
name: "step1",
|
|
76
|
+
handler: async () => {
|
|
77
|
+
order.push("step1");
|
|
78
|
+
return { a: 1 };
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "step2",
|
|
83
|
+
handler: async ({ results }) => {
|
|
84
|
+
order.push("step2");
|
|
85
|
+
return { b: 2, fromStep1: results.step1 };
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "step3",
|
|
90
|
+
handler: async ({ results }) => {
|
|
91
|
+
order.push("step3");
|
|
92
|
+
return { c: 3, fromStep2: results.step2 };
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
100
|
+
alepha.with(AlephaApiWorkflows);
|
|
101
|
+
alepha.with(App);
|
|
102
|
+
await alepha.start();
|
|
103
|
+
|
|
104
|
+
const app = alepha.inject(App);
|
|
105
|
+
const executionId = await app.myWorkflow.start({ id: "test" });
|
|
106
|
+
|
|
107
|
+
await vi.waitFor(async () => {
|
|
108
|
+
const exec = await app.repo.findById(executionId);
|
|
109
|
+
expect(exec?.status).toBe("completed");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(order).toEqual(["step1", "step2", "step3"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should pass accumulated results between steps", async () => {
|
|
116
|
+
let step2Results: Record<string, unknown> = {};
|
|
117
|
+
|
|
118
|
+
class App {
|
|
119
|
+
repo = $repository(workflowExecutions);
|
|
120
|
+
myWorkflow = $workflow({
|
|
121
|
+
schema: t.object({ value: t.text() }),
|
|
122
|
+
steps: [
|
|
123
|
+
{
|
|
124
|
+
name: "first",
|
|
125
|
+
handler: async () => ({ key: "from-first" }),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "second",
|
|
129
|
+
handler: async ({ results }) => {
|
|
130
|
+
step2Results = results;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
138
|
+
alepha.with(AlephaApiWorkflows);
|
|
139
|
+
alepha.with(App);
|
|
140
|
+
await alepha.start();
|
|
141
|
+
|
|
142
|
+
const app = alepha.inject(App);
|
|
143
|
+
const executionId = await app.myWorkflow.start({ value: "test" });
|
|
144
|
+
|
|
145
|
+
await vi.waitFor(async () => {
|
|
146
|
+
const exec = await app.repo.findById(executionId);
|
|
147
|
+
expect(exec?.status).toBe("completed");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(step2Results.first).toEqual({ key: "from-first" });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should use ClassName.propertyKey as workflow name", async () => {
|
|
154
|
+
class OrderService {
|
|
155
|
+
processOrder = $workflow({
|
|
156
|
+
schema: t.object({ id: t.text() }),
|
|
157
|
+
steps: [{ name: "step1", handler: async () => {} }],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
162
|
+
alepha.with(AlephaApiWorkflows);
|
|
163
|
+
alepha.with(OrderService);
|
|
164
|
+
await alepha.start();
|
|
165
|
+
|
|
166
|
+
const app = alepha.inject(OrderService);
|
|
167
|
+
expect(app.processOrder.name).toBe("OrderService.processOrder");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ----- Compensation (saga pattern) -----
|
|
172
|
+
|
|
173
|
+
describe("compensation", () => {
|
|
174
|
+
it("should compensate completed steps in reverse order on failure", async () => {
|
|
175
|
+
const compensations: string[] = [];
|
|
176
|
+
|
|
177
|
+
class App {
|
|
178
|
+
repo = $repository(workflowExecutions);
|
|
179
|
+
myWorkflow = $workflow({
|
|
180
|
+
schema: t.object({ id: t.text() }),
|
|
181
|
+
onError: "compensate",
|
|
182
|
+
steps: [
|
|
183
|
+
{
|
|
184
|
+
name: "step1",
|
|
185
|
+
handler: async () => ({ id: "r1" }),
|
|
186
|
+
compensate: async () => {
|
|
187
|
+
compensations.push("step1");
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "step2",
|
|
192
|
+
handler: async () => ({ id: "r2" }),
|
|
193
|
+
compensate: async () => {
|
|
194
|
+
compensations.push("step2");
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "step3",
|
|
199
|
+
handler: async () => {
|
|
200
|
+
throw new Error("step3 failed");
|
|
201
|
+
},
|
|
202
|
+
compensate: async () => {
|
|
203
|
+
compensations.push("step3");
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
211
|
+
alepha.with(AlephaApiWorkflows);
|
|
212
|
+
alepha.with(App);
|
|
213
|
+
await alepha.start();
|
|
214
|
+
|
|
215
|
+
const app = alepha.inject(App);
|
|
216
|
+
const executionId = await app.myWorkflow.start({ id: "test" });
|
|
217
|
+
|
|
218
|
+
await vi.waitFor(async () => {
|
|
219
|
+
const exec = await app.repo.findById(executionId);
|
|
220
|
+
expect(exec?.status).toBe("compensated");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// step3 never completed so no compensation for it
|
|
224
|
+
// step2 and step1 compensated in reverse order
|
|
225
|
+
expect(compensations).toEqual(["step2", "step1"]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should mark as failed when onError is fail", async () => {
|
|
229
|
+
class App {
|
|
230
|
+
repo = $repository(workflowExecutions);
|
|
231
|
+
myWorkflow = $workflow({
|
|
232
|
+
schema: t.object({ id: t.text() }),
|
|
233
|
+
onError: "fail",
|
|
234
|
+
steps: [
|
|
235
|
+
{
|
|
236
|
+
name: "step1",
|
|
237
|
+
handler: async () => ({ ok: true }),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "step2",
|
|
241
|
+
handler: async () => {
|
|
242
|
+
throw new Error("boom");
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
250
|
+
alepha.with(AlephaApiWorkflows);
|
|
251
|
+
alepha.with(App);
|
|
252
|
+
await alepha.start();
|
|
253
|
+
|
|
254
|
+
const app = alepha.inject(App);
|
|
255
|
+
const executionId = await app.myWorkflow.start({ id: "test" });
|
|
256
|
+
|
|
257
|
+
await vi.waitFor(async () => {
|
|
258
|
+
const exec = await app.repo.findById(executionId);
|
|
259
|
+
expect(exec?.status).toBe("failed");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const exec = await app.repo.findById(executionId);
|
|
263
|
+
expect(exec?.error).toBe("boom");
|
|
264
|
+
expect(exec?.errorStep).toBe("step2");
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ----- Retry -----
|
|
269
|
+
|
|
270
|
+
describe("retry", () => {
|
|
271
|
+
it("should retry a step on failure with retries configured", async () => {
|
|
272
|
+
let callCount = 0;
|
|
273
|
+
|
|
274
|
+
class App {
|
|
275
|
+
repo = $repository(workflowExecutions);
|
|
276
|
+
stepRepo = $repository(workflowStepExecutions);
|
|
277
|
+
myWorkflow = $workflow({
|
|
278
|
+
schema: t.object({ id: t.text() }),
|
|
279
|
+
steps: [
|
|
280
|
+
{
|
|
281
|
+
name: "flaky",
|
|
282
|
+
retry: { retries: 2, backoff: [10, "millisecond"] },
|
|
283
|
+
handler: async () => {
|
|
284
|
+
callCount++;
|
|
285
|
+
if (callCount < 3) throw new Error(`fail #${callCount}`);
|
|
286
|
+
return { ok: true };
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
294
|
+
alepha.with(AlephaApiWorkflows);
|
|
295
|
+
alepha.with(App);
|
|
296
|
+
await alepha.start();
|
|
297
|
+
|
|
298
|
+
const app = alepha.inject(App);
|
|
299
|
+
const executionId = await app.myWorkflow.start({ id: "test" });
|
|
300
|
+
|
|
301
|
+
await vi.waitFor(
|
|
302
|
+
async () => {
|
|
303
|
+
const exec = await app.repo.findById(executionId);
|
|
304
|
+
expect(exec?.status).toBe("completed");
|
|
305
|
+
},
|
|
306
|
+
{ timeout: 10_000 },
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
expect(callCount).toBe(3);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ----- Conditional steps -----
|
|
314
|
+
|
|
315
|
+
describe("conditional steps", () => {
|
|
316
|
+
it("should skip steps when condition returns false", async () => {
|
|
317
|
+
const executed: string[] = [];
|
|
318
|
+
|
|
319
|
+
class App {
|
|
320
|
+
repo = $repository(workflowExecutions);
|
|
321
|
+
stepRepo = $repository(workflowStepExecutions);
|
|
322
|
+
myWorkflow = $workflow({
|
|
323
|
+
schema: t.object({ skipMiddle: t.boolean() }),
|
|
324
|
+
steps: [
|
|
325
|
+
{
|
|
326
|
+
name: "step1",
|
|
327
|
+
handler: async () => {
|
|
328
|
+
executed.push("step1");
|
|
329
|
+
return { value: 1 };
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "step2",
|
|
334
|
+
when: ({ payload }) => !payload.skipMiddle,
|
|
335
|
+
handler: async () => {
|
|
336
|
+
executed.push("step2");
|
|
337
|
+
return { value: 2 };
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "step3",
|
|
342
|
+
handler: async () => {
|
|
343
|
+
executed.push("step3");
|
|
344
|
+
return { value: 3 };
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
352
|
+
alepha.with(AlephaApiWorkflows);
|
|
353
|
+
alepha.with(App);
|
|
354
|
+
await alepha.start();
|
|
355
|
+
|
|
356
|
+
const app = alepha.inject(App);
|
|
357
|
+
const executionId = await app.myWorkflow.start({ skipMiddle: true });
|
|
358
|
+
|
|
359
|
+
await vi.waitFor(async () => {
|
|
360
|
+
const exec = await app.repo.findById(executionId);
|
|
361
|
+
expect(exec?.status).toBe("completed");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(executed).toEqual(["step1", "step3"]);
|
|
365
|
+
|
|
366
|
+
// Verify step2 was skipped
|
|
367
|
+
const steps = await app.stepRepo.findMany({
|
|
368
|
+
where: { workflowExecutionId: { eq: executionId } },
|
|
369
|
+
orderBy: { column: "stepIndex", direction: "asc" },
|
|
370
|
+
});
|
|
371
|
+
expect(steps[1].status).toBe("skipped");
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// ----- Cancel -----
|
|
376
|
+
|
|
377
|
+
describe("cancel", () => {
|
|
378
|
+
it("should cancel a pending workflow", async () => {
|
|
379
|
+
class App {
|
|
380
|
+
repo = $repository(workflowExecutions);
|
|
381
|
+
stepRepo = $repository(workflowStepExecutions);
|
|
382
|
+
myWorkflow = $workflow({
|
|
383
|
+
schema: t.object({ id: t.text() }),
|
|
384
|
+
steps: [
|
|
385
|
+
{
|
|
386
|
+
name: "long",
|
|
387
|
+
handler: async ({ signal }) => {
|
|
388
|
+
await new Promise<void>((resolve) => {
|
|
389
|
+
const check = () => {
|
|
390
|
+
if (signal.aborted) resolve();
|
|
391
|
+
else setTimeout(check, 10);
|
|
392
|
+
};
|
|
393
|
+
check();
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
402
|
+
alepha.with(AlephaApiWorkflows);
|
|
403
|
+
alepha.with(App);
|
|
404
|
+
await alepha.start();
|
|
405
|
+
|
|
406
|
+
const app = alepha.inject(App);
|
|
407
|
+
const executionId = await app.myWorkflow.start({ id: "test" });
|
|
408
|
+
|
|
409
|
+
// Wait for it to be running
|
|
410
|
+
await vi.waitFor(async () => {
|
|
411
|
+
const steps = await app.stepRepo.findMany({
|
|
412
|
+
where: {
|
|
413
|
+
workflowExecutionId: { eq: executionId },
|
|
414
|
+
status: { eq: "running" },
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
expect(steps).toHaveLength(1);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
await app.myWorkflow.cancel(executionId);
|
|
421
|
+
|
|
422
|
+
await vi.waitFor(async () => {
|
|
423
|
+
const exec = await app.repo.findById(executionId);
|
|
424
|
+
expect(exec?.status).toBe("cancelled");
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// ----- Retry (admin action) -----
|
|
430
|
+
|
|
431
|
+
describe("admin retry", () => {
|
|
432
|
+
it("should retry a failed workflow from the failed step", async () => {
|
|
433
|
+
let callCount = 0;
|
|
434
|
+
|
|
435
|
+
class App {
|
|
436
|
+
repo = $repository(workflowExecutions);
|
|
437
|
+
myWorkflow = $workflow({
|
|
438
|
+
schema: t.object({ id: t.text() }),
|
|
439
|
+
onError: "fail",
|
|
440
|
+
steps: [
|
|
441
|
+
{
|
|
442
|
+
name: "step1",
|
|
443
|
+
handler: async () => ({ ok: true }),
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "step2",
|
|
447
|
+
handler: async () => {
|
|
448
|
+
callCount++;
|
|
449
|
+
if (callCount === 1) throw new Error("transient");
|
|
450
|
+
return { ok: true };
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
458
|
+
alepha.with(AlephaApiWorkflows);
|
|
459
|
+
alepha.with(App);
|
|
460
|
+
await alepha.start();
|
|
461
|
+
|
|
462
|
+
const app = alepha.inject(App);
|
|
463
|
+
const executionId = await app.myWorkflow.start({ id: "test" });
|
|
464
|
+
|
|
465
|
+
await vi.waitFor(async () => {
|
|
466
|
+
const exec = await app.repo.findById(executionId);
|
|
467
|
+
expect(exec?.status).toBe("failed");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Retry from the failed step
|
|
471
|
+
await app.myWorkflow.retry(executionId);
|
|
472
|
+
|
|
473
|
+
await vi.waitFor(async () => {
|
|
474
|
+
const exec = await app.repo.findById(executionId);
|
|
475
|
+
expect(exec?.status).toBe("completed");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(callCount).toBe(2);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// ----- Restart -----
|
|
483
|
+
|
|
484
|
+
describe("restart", () => {
|
|
485
|
+
it("should create a new execution from the same payload", async () => {
|
|
486
|
+
let callCount = 0;
|
|
487
|
+
|
|
488
|
+
class App {
|
|
489
|
+
repo = $repository(workflowExecutions);
|
|
490
|
+
myWorkflow = $workflow({
|
|
491
|
+
schema: t.object({ id: t.text() }),
|
|
492
|
+
onError: "fail",
|
|
493
|
+
steps: [
|
|
494
|
+
{
|
|
495
|
+
name: "step1",
|
|
496
|
+
handler: async () => {
|
|
497
|
+
callCount++;
|
|
498
|
+
if (callCount === 1) throw new Error("fail first time");
|
|
499
|
+
return { ok: true };
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
507
|
+
alepha.with(AlephaApiWorkflows);
|
|
508
|
+
alepha.with(App);
|
|
509
|
+
await alepha.start();
|
|
510
|
+
|
|
511
|
+
const app = alepha.inject(App);
|
|
512
|
+
const executionId = await app.myWorkflow.start({ id: "test" });
|
|
513
|
+
|
|
514
|
+
await vi.waitFor(async () => {
|
|
515
|
+
const exec = await app.repo.findById(executionId);
|
|
516
|
+
expect(exec?.status).toBe("failed");
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const newId = await app.myWorkflow.restart(executionId);
|
|
520
|
+
expect(newId).not.toBe(executionId);
|
|
521
|
+
|
|
522
|
+
await vi.waitFor(async () => {
|
|
523
|
+
const exec = await app.repo.findById(newId);
|
|
524
|
+
expect(exec?.status).toBe("completed");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
expect(callCount).toBe(2);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// ----- Events -----
|
|
532
|
+
|
|
533
|
+
describe("events", () => {
|
|
534
|
+
it("should emit workflow lifecycle events", async () => {
|
|
535
|
+
const events: string[] = [];
|
|
536
|
+
|
|
537
|
+
class App {
|
|
538
|
+
repo = $repository(workflowExecutions);
|
|
539
|
+
myWorkflow = $workflow({
|
|
540
|
+
schema: t.object({ id: t.text() }),
|
|
541
|
+
steps: [{ name: "step1", handler: async () => ({ ok: true }) }],
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
546
|
+
alepha.with(AlephaApiWorkflows);
|
|
547
|
+
alepha.with(App);
|
|
548
|
+
alepha.events.on("workflow:started", () => {
|
|
549
|
+
events.push("started");
|
|
550
|
+
});
|
|
551
|
+
alepha.events.on("workflow:step:begin", () => {
|
|
552
|
+
events.push("step:begin");
|
|
553
|
+
});
|
|
554
|
+
alepha.events.on("workflow:step:completed", () => {
|
|
555
|
+
events.push("step:completed");
|
|
556
|
+
});
|
|
557
|
+
alepha.events.on("workflow:completed", () => {
|
|
558
|
+
events.push("completed");
|
|
559
|
+
});
|
|
560
|
+
await alepha.start();
|
|
561
|
+
|
|
562
|
+
const app = alepha.inject(App);
|
|
563
|
+
const executionId = await app.myWorkflow.start({ id: "test" });
|
|
564
|
+
|
|
565
|
+
await vi.waitFor(async () => {
|
|
566
|
+
const exec = await app.repo.findById(executionId);
|
|
567
|
+
expect(exec?.status).toBe("completed");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
expect(events).toContain("started");
|
|
571
|
+
expect(events).toContain("step:begin");
|
|
572
|
+
expect(events).toContain("step:completed");
|
|
573
|
+
expect(events).toContain("completed");
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// ----- Deduplication -----
|
|
578
|
+
|
|
579
|
+
describe("deduplication", () => {
|
|
580
|
+
it("should return existing execution for same key", async () => {
|
|
581
|
+
class App {
|
|
582
|
+
repo = $repository(workflowExecutions);
|
|
583
|
+
myWorkflow = $workflow({
|
|
584
|
+
schema: t.object({ id: t.text() }),
|
|
585
|
+
steps: [{ name: "step1", handler: async () => ({ ok: true }) }],
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
590
|
+
alepha.with(AlephaApiWorkflows);
|
|
591
|
+
alepha.with(App);
|
|
592
|
+
await alepha.start();
|
|
593
|
+
|
|
594
|
+
const app = alepha.inject(App);
|
|
595
|
+
|
|
596
|
+
// First start with key — creates a new execution
|
|
597
|
+
const id1 = await app.myWorkflow.start(
|
|
598
|
+
{ id: "test" },
|
|
599
|
+
{ key: "dedup-key" },
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
// Wait for completion (key is cleared on completion)
|
|
603
|
+
await vi.waitFor(async () => {
|
|
604
|
+
const exec = await app.repo.findById(id1);
|
|
605
|
+
expect(exec?.status).toBe("completed");
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Second start with same key after completion — creates NEW execution
|
|
609
|
+
const id2 = await app.myWorkflow.start(
|
|
610
|
+
{ id: "test" },
|
|
611
|
+
{ key: "dedup-key" },
|
|
612
|
+
);
|
|
613
|
+
expect(id2).not.toBe(id1);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
});
|