@startsimpli/api 0.5.21 → 0.5.22
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/package.json +1 -1
- package/src/index.ts +34 -0
- package/src/lib/presentations-api.test.ts +104 -0
- package/src/lib/presentations-api.ts +166 -0
- package/src/lib/workflows-api.test.ts +129 -0
- package/src/lib/workflows-api.ts +161 -115
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -18,6 +18,21 @@ export { ContactsApi } from './lib/contacts-api';
|
|
|
18
18
|
export { OrganizationsApi } from './lib/organizations-api';
|
|
19
19
|
export { EntitiesApi } from './lib/entities-api';
|
|
20
20
|
export { WorkflowsApi } from './lib/workflows-api';
|
|
21
|
+
export type {
|
|
22
|
+
Workflow,
|
|
23
|
+
WorkflowNode,
|
|
24
|
+
WorkflowInput,
|
|
25
|
+
WorkflowListParams,
|
|
26
|
+
NodeTypeDef,
|
|
27
|
+
NodeStatus,
|
|
28
|
+
RunStatus,
|
|
29
|
+
ExecutionMode,
|
|
30
|
+
WorkflowExecution,
|
|
31
|
+
WorkflowNodeExecution,
|
|
32
|
+
WorkflowExecutionDetail,
|
|
33
|
+
ExecuteWorkflowResponse,
|
|
34
|
+
ExecutionListParams,
|
|
35
|
+
} from './lib/workflows-api';
|
|
21
36
|
export { MessagesApi } from './lib/messages-api';
|
|
22
37
|
export type { Message, MessageStatus as MessageApiStatus, MessageRecipient, MessagingChannel as MessagingChannelType, MessageFilters, CreateMessageInput, ScheduleMessageInput, SendTestInput } from './lib/messages-api';
|
|
23
38
|
export { calculateStatsFromMessages } from './lib/message-stats';
|
|
@@ -164,6 +179,7 @@ import { TargetListsApi } from './lib/target-lists-api';
|
|
|
164
179
|
import { MessageTemplatesApi } from './lib/message-templates-api';
|
|
165
180
|
import { MarketsApi } from './lib/markets-api';
|
|
166
181
|
import { VaultApi } from './lib/vault-api';
|
|
182
|
+
import { PresentationsApi } from './lib/presentations-api';
|
|
167
183
|
import { CompaniesApi } from './lib/companies-api';
|
|
168
184
|
import { TeamsApi } from './lib/teams-api';
|
|
169
185
|
import { TeamInvitationsApi } from './lib/team-invitations-api';
|
|
@@ -193,6 +209,7 @@ export function createStartSimpliApi(config: ApiClientConfig = {}) {
|
|
|
193
209
|
messageTemplates: new MessageTemplatesApi(client),
|
|
194
210
|
markets: new MarketsApi(client),
|
|
195
211
|
vault: new VaultApi(client),
|
|
212
|
+
presentations: new PresentationsApi(client),
|
|
196
213
|
companies: new CompaniesApi(client),
|
|
197
214
|
teams: new TeamsApi(client),
|
|
198
215
|
teamInvitations: new TeamInvitationsApi(client),
|
|
@@ -222,6 +239,23 @@ export type {
|
|
|
222
239
|
VaultListParams,
|
|
223
240
|
} from './lib/vault-api';
|
|
224
241
|
|
|
242
|
+
// Presentations API
|
|
243
|
+
export { PresentationsApi } from './lib/presentations-api';
|
|
244
|
+
export type {
|
|
245
|
+
Deck,
|
|
246
|
+
DeckInput,
|
|
247
|
+
DeckStatus,
|
|
248
|
+
DeckListParams,
|
|
249
|
+
DeckGenerationStatus,
|
|
250
|
+
GenerateDeckInput,
|
|
251
|
+
Slide,
|
|
252
|
+
SlideInput,
|
|
253
|
+
SlideStatus,
|
|
254
|
+
RegenerateSlideInput,
|
|
255
|
+
RenderMode,
|
|
256
|
+
CompileResult,
|
|
257
|
+
} from './lib/presentations-api';
|
|
258
|
+
|
|
225
259
|
// Markets API
|
|
226
260
|
export { MarketsApi } from './lib/markets-api';
|
|
227
261
|
export type {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { PresentationsApi } from './presentations-api';
|
|
3
|
+
|
|
4
|
+
function makeApi() {
|
|
5
|
+
const fetch = { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() };
|
|
6
|
+
const api = new PresentationsApi({ fetch } as never);
|
|
7
|
+
return { api, fetch };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('PresentationsApi', () => {
|
|
11
|
+
let api: PresentationsApi;
|
|
12
|
+
let fetch: { get: ReturnType<typeof vi.fn>; post: ReturnType<typeof vi.fn>; patch: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> };
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
({ api, fetch } = makeApi());
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('listDecks hits the decks endpoint with params', async () => {
|
|
19
|
+
fetch.get.mockResolvedValue({ results: [], count: 0 });
|
|
20
|
+
await api.listDecks({ page: 2 });
|
|
21
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/presentations/decks', { params: { page: 2 } });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('getDeck fetches a single deck by id', async () => {
|
|
25
|
+
fetch.get.mockResolvedValue({ id: 'd1', title: 'Pitch' });
|
|
26
|
+
const d = await api.getDeck('d1');
|
|
27
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/presentations/decks/d1');
|
|
28
|
+
expect(d.title).toBe('Pitch');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('createDeck posts the deck input', async () => {
|
|
32
|
+
fetch.post.mockResolvedValue({ id: 'd1', title: 'Pitch' });
|
|
33
|
+
await api.createDeck({ title: 'Pitch', outline: 'Slide 1...', audience: 'VCs' });
|
|
34
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks', {
|
|
35
|
+
title: 'Pitch',
|
|
36
|
+
outline: 'Slide 1...',
|
|
37
|
+
audience: 'VCs',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('updateDeck patches by id', async () => {
|
|
42
|
+
fetch.patch.mockResolvedValue({ id: 'd1', title: 'New' });
|
|
43
|
+
await api.updateDeck('d1', { title: 'New' });
|
|
44
|
+
expect(fetch.patch).toHaveBeenCalledWith('api/v1/presentations/decks/d1', { title: 'New' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('deleteDeck deletes by id', async () => {
|
|
48
|
+
fetch.delete.mockResolvedValue(undefined);
|
|
49
|
+
await api.deleteDeck('d1');
|
|
50
|
+
expect(fetch.delete).toHaveBeenCalledWith('api/v1/presentations/decks/d1');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('generateDeck kicks off generation (derive style + slides)', async () => {
|
|
54
|
+
fetch.post.mockResolvedValue({ id: 'd1', status: 'generating' });
|
|
55
|
+
await api.generateDeck('d1', { outline: 'Slide 1: Title' });
|
|
56
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks/d1/generate', {
|
|
57
|
+
outline: 'Slide 1: Title',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('getDeckStatus polls the status endpoint', async () => {
|
|
62
|
+
fetch.get.mockResolvedValue({ status: 'generating', slides: [] });
|
|
63
|
+
await api.getDeckStatus('d1');
|
|
64
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/presentations/decks/d1/status');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('listSlides fetches the deck slides', async () => {
|
|
68
|
+
fetch.get.mockResolvedValue([{ id: 's1', slideNumber: 1 }]);
|
|
69
|
+
await api.listSlides('d1');
|
|
70
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/presentations/decks/d1/slides');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('updateSlide patches a slide by number', async () => {
|
|
74
|
+
fetch.patch.mockResolvedValue({ id: 's2', slideNumber: 2 });
|
|
75
|
+
await api.updateSlide('d1', 2, { content: 'Revised' });
|
|
76
|
+
expect(fetch.patch).toHaveBeenCalledWith('api/v1/presentations/decks/d1/slides/2', {
|
|
77
|
+
content: 'Revised',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('regenerateSlide posts an instruction for one slide', async () => {
|
|
82
|
+
fetch.post.mockResolvedValue({ id: 's2', slideNumber: 2, status: 'generating' });
|
|
83
|
+
await api.regenerateSlide('d1', 2, { instruction: 'make it punchier' });
|
|
84
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks/d1/slides/2/regenerate', {
|
|
85
|
+
instruction: 'make it punchier',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('reorderSlides posts the new order', async () => {
|
|
90
|
+
fetch.post.mockResolvedValue(undefined);
|
|
91
|
+
await api.reorderSlides('d1', [3, 1, 2]);
|
|
92
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks/d1/slides/reorder', {
|
|
93
|
+
order: [3, 1, 2],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('compileDeck builds PPTX + PDF and returns artifact urls', async () => {
|
|
98
|
+
fetch.post.mockResolvedValue({ status: 'ready', pptxUrl: 'p.pptx', pdfUrl: 'p.pdf' });
|
|
99
|
+
const r = await api.compileDeck('d1');
|
|
100
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks/d1/compile', undefined);
|
|
101
|
+
expect(r.pptxUrl).toBe('p.pptx');
|
|
102
|
+
expect(r.pdfUrl).toBe('p.pdf');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** Presentations API client — AI-generated slide decks (epic startsim-3ks).
|
|
2
|
+
|
|
3
|
+
Mirrors the other @startsimpli/api wrappers and the proven n8n "Slide Deck
|
|
4
|
+
Agent v9" pipeline: a deck has ONE derived style spec applied verbatim to every
|
|
5
|
+
slide; each slide is a full-bleed 16:9 image (image render mode), compiled into
|
|
6
|
+
PPTX + PDF. The client auto-transforms snake_case <-> camelCase, so types here
|
|
7
|
+
are camelCase.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PaginatedResponse } from '../types';
|
|
11
|
+
import type { ApiClient } from './api-client';
|
|
12
|
+
|
|
13
|
+
const E = {
|
|
14
|
+
decks: 'api/v1/presentations/decks',
|
|
15
|
+
deck: (id: string) => `api/v1/presentations/decks/${id}`,
|
|
16
|
+
generate: (id: string) => `api/v1/presentations/decks/${id}/generate`,
|
|
17
|
+
compile: (id: string) => `api/v1/presentations/decks/${id}/compile`,
|
|
18
|
+
status: (id: string) => `api/v1/presentations/decks/${id}/status`,
|
|
19
|
+
slides: (id: string) => `api/v1/presentations/decks/${id}/slides`,
|
|
20
|
+
slide: (id: string, n: number) => `api/v1/presentations/decks/${id}/slides/${n}`,
|
|
21
|
+
regenerate: (id: string, n: number) => `api/v1/presentations/decks/${id}/slides/${n}/regenerate`,
|
|
22
|
+
reorder: (id: string) => `api/v1/presentations/decks/${id}/slides/reorder`,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type DeckStatus = 'draft' | 'generating' | 'ready' | 'error';
|
|
26
|
+
export type SlideStatus = 'pending' | 'generating' | 'ready' | 'error';
|
|
27
|
+
export type RenderMode = 'image' | 'structured';
|
|
28
|
+
|
|
29
|
+
export interface Slide {
|
|
30
|
+
id: string;
|
|
31
|
+
deckId: string;
|
|
32
|
+
slideNumber: number;
|
|
33
|
+
title?: string;
|
|
34
|
+
/** Raw text content / outline for this slide. */
|
|
35
|
+
content: string;
|
|
36
|
+
/** The full image-generation prompt (style spec + slide content). */
|
|
37
|
+
imagePrompt?: string;
|
|
38
|
+
/** Full-bleed slide image (image render mode). */
|
|
39
|
+
imageUrl?: string;
|
|
40
|
+
status: SlideStatus;
|
|
41
|
+
renderMode: RenderMode;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Deck {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
/** Pasted outline the deck was generated from. */
|
|
50
|
+
outline: string;
|
|
51
|
+
/** Free-text audience / context that informs the style spec. */
|
|
52
|
+
audience: string;
|
|
53
|
+
/** The single derived STYLE SPEC, reused verbatim on every slide. */
|
|
54
|
+
styleSpec: string;
|
|
55
|
+
status: DeckStatus;
|
|
56
|
+
renderMode: RenderMode;
|
|
57
|
+
slideCount: number;
|
|
58
|
+
/** Compiled artifacts (present once compiled). */
|
|
59
|
+
pptxUrl?: string | null;
|
|
60
|
+
pdfUrl?: string | null;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
updatedAt: string;
|
|
63
|
+
/** Present on detail responses. */
|
|
64
|
+
slides?: Slide[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DeckInput {
|
|
68
|
+
title: string;
|
|
69
|
+
outline?: string;
|
|
70
|
+
audience?: string;
|
|
71
|
+
renderMode?: RenderMode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface GenerateDeckInput {
|
|
75
|
+
/** Outline to (re)generate from; defaults to the deck's stored outline. */
|
|
76
|
+
outline?: string;
|
|
77
|
+
/** Optional explicit style spec; otherwise the backend derives one. */
|
|
78
|
+
styleSpec?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SlideInput {
|
|
82
|
+
title?: string;
|
|
83
|
+
content?: string;
|
|
84
|
+
imagePrompt?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RegenerateSlideInput {
|
|
88
|
+
/** Revised content for the slide. */
|
|
89
|
+
content?: string;
|
|
90
|
+
/** Natural-language change request ("make it punchier"). */
|
|
91
|
+
instruction?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Lightweight per-slide progress for polling during generation. */
|
|
95
|
+
export interface DeckGenerationStatus {
|
|
96
|
+
status: DeckStatus;
|
|
97
|
+
slides: Array<{
|
|
98
|
+
slideNumber: number;
|
|
99
|
+
status: SlideStatus;
|
|
100
|
+
imageUrl?: string;
|
|
101
|
+
}>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface CompileResult {
|
|
105
|
+
status: DeckStatus;
|
|
106
|
+
pptxUrl: string;
|
|
107
|
+
pdfUrl: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface DeckListParams {
|
|
111
|
+
page?: number;
|
|
112
|
+
pageSize?: number;
|
|
113
|
+
search?: string;
|
|
114
|
+
ordering?: string;
|
|
115
|
+
[key: string]: unknown;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class PresentationsApi {
|
|
119
|
+
constructor(private client: ApiClient) {}
|
|
120
|
+
|
|
121
|
+
// --- Decks ---
|
|
122
|
+
listDecks(params?: DeckListParams): Promise<PaginatedResponse<Deck>> {
|
|
123
|
+
return this.client.fetch.get<PaginatedResponse<Deck>>(E.decks, { params });
|
|
124
|
+
}
|
|
125
|
+
getDeck(id: string): Promise<Deck> {
|
|
126
|
+
return this.client.fetch.get<Deck>(E.deck(id));
|
|
127
|
+
}
|
|
128
|
+
createDeck(data: DeckInput): Promise<Deck> {
|
|
129
|
+
return this.client.fetch.post<Deck>(E.decks, data);
|
|
130
|
+
}
|
|
131
|
+
updateDeck(id: string, data: Partial<DeckInput>): Promise<Deck> {
|
|
132
|
+
return this.client.fetch.patch<Deck>(E.deck(id), data);
|
|
133
|
+
}
|
|
134
|
+
deleteDeck(id: string): Promise<void> {
|
|
135
|
+
return this.client.fetch.delete<void>(E.deck(id));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Generation pipeline (mirrors the n8n agent tools) ---
|
|
139
|
+
/** Derive the style spec (if absent) and generate every slide image async. */
|
|
140
|
+
generateDeck(id: string, data?: GenerateDeckInput): Promise<Deck> {
|
|
141
|
+
return this.client.fetch.post<Deck>(E.generate(id), data);
|
|
142
|
+
}
|
|
143
|
+
/** Per-slide progress, for polling while generation/compile runs. */
|
|
144
|
+
getDeckStatus(id: string): Promise<DeckGenerationStatus> {
|
|
145
|
+
return this.client.fetch.get<DeckGenerationStatus>(E.status(id));
|
|
146
|
+
}
|
|
147
|
+
/** Build the Google-Slides-equivalent deck and export PPTX + PDF. */
|
|
148
|
+
compileDeck(id: string): Promise<CompileResult> {
|
|
149
|
+
return this.client.fetch.post<CompileResult>(E.compile(id), undefined);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Slides ---
|
|
153
|
+
listSlides(id: string): Promise<Slide[]> {
|
|
154
|
+
return this.client.fetch.get<Slide[]>(E.slides(id));
|
|
155
|
+
}
|
|
156
|
+
updateSlide(id: string, slideNumber: number, data: SlideInput): Promise<Slide> {
|
|
157
|
+
return this.client.fetch.patch<Slide>(E.slide(id, slideNumber), data);
|
|
158
|
+
}
|
|
159
|
+
/** Regenerate a single slide image (keeps the deck style spec). */
|
|
160
|
+
regenerateSlide(id: string, slideNumber: number, data?: RegenerateSlideInput): Promise<Slide> {
|
|
161
|
+
return this.client.fetch.post<Slide>(E.regenerate(id, slideNumber), data);
|
|
162
|
+
}
|
|
163
|
+
reorderSlides(id: string, order: number[]): Promise<void> {
|
|
164
|
+
return this.client.fetch.post<void>(E.reorder(id), { order });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { WorkflowsApi } from './workflows-api';
|
|
3
|
+
|
|
4
|
+
function makeApi() {
|
|
5
|
+
const fetch = { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() };
|
|
6
|
+
const api = new WorkflowsApi({ fetch } as never);
|
|
7
|
+
return { api, fetch };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('WorkflowsApi', () => {
|
|
11
|
+
let api: WorkflowsApi;
|
|
12
|
+
let fetch: {
|
|
13
|
+
get: ReturnType<typeof vi.fn>;
|
|
14
|
+
post: ReturnType<typeof vi.fn>;
|
|
15
|
+
patch: ReturnType<typeof vi.fn>;
|
|
16
|
+
delete: ReturnType<typeof vi.fn>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
({ api, fetch } = makeApi());
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// --- Workflows CRUD ---
|
|
24
|
+
it('listWorkflows hits the workflows endpoint with params', async () => {
|
|
25
|
+
fetch.get.mockResolvedValue({ results: [], count: 0 });
|
|
26
|
+
await api.listWorkflows({ page: 2 });
|
|
27
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows', { params: { page: 2 } });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('getWorkflow fetches a single workflow by id', async () => {
|
|
31
|
+
fetch.get.mockResolvedValue({ id: 'w1', name: 'Onboarding' });
|
|
32
|
+
const w = await api.getWorkflow('w1');
|
|
33
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/w1');
|
|
34
|
+
expect(w.name).toBe('Onboarding');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('createWorkflow posts the workflow input', async () => {
|
|
38
|
+
fetch.post.mockResolvedValue({ id: 'w1', name: 'Onboarding' });
|
|
39
|
+
await api.createWorkflow({ name: 'Onboarding', description: 'desc' });
|
|
40
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows', {
|
|
41
|
+
name: 'Onboarding',
|
|
42
|
+
description: 'desc',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('updateWorkflow patches by id', async () => {
|
|
47
|
+
fetch.patch.mockResolvedValue({ id: 'w1', name: 'New' });
|
|
48
|
+
await api.updateWorkflow('w1', { name: 'New' });
|
|
49
|
+
expect(fetch.patch).toHaveBeenCalledWith('api/v1/workflows/w1', { name: 'New' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('deleteWorkflow deletes by id', async () => {
|
|
53
|
+
fetch.delete.mockResolvedValue(undefined);
|
|
54
|
+
await api.deleteWorkflow('w1');
|
|
55
|
+
expect(fetch.delete).toHaveBeenCalledWith('api/v1/workflows/w1');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- Node types ---
|
|
59
|
+
it('getNodeTypes fetches the node-type registry', async () => {
|
|
60
|
+
fetch.get.mockResolvedValue([
|
|
61
|
+
{ slug: 'http', category: 'action', label: 'HTTP Request', parameterSchema: {}, outputLabels: ['main'] },
|
|
62
|
+
]);
|
|
63
|
+
const types = await api.getNodeTypes();
|
|
64
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/node-types');
|
|
65
|
+
expect(types[0].slug).toBe('http');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// --- Execution ---
|
|
69
|
+
it('executeWorkflow posts context and returns an executionId', async () => {
|
|
70
|
+
fetch.post.mockResolvedValue({ executionId: 'e1', status: 'running' });
|
|
71
|
+
const res = await api.executeWorkflow('w1', { foo: 'bar' });
|
|
72
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/w1/execute', { contextData: { foo: 'bar' } });
|
|
73
|
+
expect(res.executionId).toBe('e1');
|
|
74
|
+
expect(res.status).toBe('running');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('executeWorkflow works with no context data', async () => {
|
|
78
|
+
fetch.post.mockResolvedValue({ executionId: 'e2', status: 'pending' });
|
|
79
|
+
await api.executeWorkflow('w1');
|
|
80
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/w1/execute', { contextData: undefined });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('getExecution fetches a single execution detail by executionId', async () => {
|
|
84
|
+
fetch.get.mockResolvedValue({ status: 'running', state: {}, nodeExecutions: [] });
|
|
85
|
+
const e = await api.getExecution('e1');
|
|
86
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/executions/e1');
|
|
87
|
+
expect(e.status).toBe('running');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('listExecutions fetches executions for a workflow with params', async () => {
|
|
91
|
+
fetch.get.mockResolvedValue({ results: [], count: 0 });
|
|
92
|
+
await api.listExecutions('w1', { page: 1 });
|
|
93
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/w1/executions', { params: { page: 1 } });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('cancelExecution posts to the cancel endpoint', async () => {
|
|
97
|
+
fetch.post.mockResolvedValue({ id: 'e1', status: 'cancelled' });
|
|
98
|
+
await api.cancelExecution('e1');
|
|
99
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/executions/e1/cancel', undefined);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('retryExecution posts to the retry endpoint and returns an executionId', async () => {
|
|
103
|
+
fetch.post.mockResolvedValue({ executionId: 'e2', status: 'pending' });
|
|
104
|
+
const res = await api.retryExecution('e1');
|
|
105
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/executions/e1/retry', undefined);
|
|
106
|
+
expect(res.executionId).toBe('e2');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// --- Lifecycle ---
|
|
110
|
+
it('cloneWorkflow posts to the clone endpoint', async () => {
|
|
111
|
+
fetch.post.mockResolvedValue({ id: 'w2', name: 'Onboarding (copy)' });
|
|
112
|
+
const w = await api.cloneWorkflow('w1');
|
|
113
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/w1/clone', undefined);
|
|
114
|
+
expect(w.id).toBe('w2');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('publishWorkflow posts to the publish endpoint', async () => {
|
|
118
|
+
fetch.post.mockResolvedValue({ id: 'w1', isActive: true });
|
|
119
|
+
await api.publishWorkflow('w1');
|
|
120
|
+
expect(fetch.post).toHaveBeenCalledWith('api/v1/workflows/w1/publish', undefined);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('getVersions fetches the workflow version history', async () => {
|
|
124
|
+
fetch.get.mockResolvedValue([{ id: 'w1', version: 1 }]);
|
|
125
|
+
const versions = await api.getVersions('w1');
|
|
126
|
+
expect(fetch.get).toHaveBeenCalledWith('api/v1/workflows/w1/versions');
|
|
127
|
+
expect(versions[0].version).toBe(1);
|
|
128
|
+
});
|
|
129
|
+
});
|
package/src/lib/workflows-api.ts
CHANGED
|
@@ -1,159 +1,205 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflows API wrapper
|
|
1
|
+
/** Workflows API client — generic node-based workflow engine (epic startsim-xsh.11).
|
|
3
2
|
*
|
|
4
|
-
*
|
|
3
|
+
* Mirrors the other @startsimpli/api wrappers: an E-map of endpoints under
|
|
4
|
+
* `api/v1/workflows/`, the `client.fetch.{get,post,patch,delete}` convention,
|
|
5
|
+
* and the auto snake_case <-> camelCase transform (so all types here are
|
|
6
|
+
* camelCase).
|
|
7
|
+
*
|
|
8
|
+
* The execution detail type (`WorkflowExecutionDetail`) is deliberately shaped
|
|
9
|
+
* to match present-web's `run-to-builder-state.ts` `WorkflowRunDetail`, so a
|
|
10
|
+
* `getExecution(...)` response can be fed straight into `runToBuilderState`.
|
|
5
11
|
*/
|
|
6
12
|
|
|
13
|
+
import type { PaginatedResponse } from '../types';
|
|
7
14
|
import type { ApiClient } from './api-client';
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
const E = {
|
|
17
|
+
workflows: 'api/v1/workflows',
|
|
18
|
+
workflow: (id: string) => `api/v1/workflows/${id}`,
|
|
19
|
+
nodeTypes: 'api/v1/workflows/node-types',
|
|
20
|
+
execute: (id: string) => `api/v1/workflows/${id}/execute`,
|
|
21
|
+
executions: (id: string) => `api/v1/workflows/${id}/executions`,
|
|
22
|
+
execution: (executionId: string) => `api/v1/workflows/executions/${executionId}`,
|
|
23
|
+
cancel: (executionId: string) => `api/v1/workflows/executions/${executionId}/cancel`,
|
|
24
|
+
retry: (executionId: string) => `api/v1/workflows/executions/${executionId}/retry`,
|
|
25
|
+
clone: (id: string) => `api/v1/workflows/${id}/clone`,
|
|
26
|
+
publish: (id: string) => `api/v1/workflows/${id}/publish`,
|
|
27
|
+
versions: (id: string) => `api/v1/workflows/${id}/versions`,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Run lifecycle status — matches run-to-builder-state's `WorkflowRunStatus`. */
|
|
31
|
+
export type RunStatus =
|
|
32
|
+
| 'pending'
|
|
33
|
+
| 'running'
|
|
34
|
+
| 'completed'
|
|
35
|
+
| 'failed'
|
|
36
|
+
| 'cancelled'
|
|
37
|
+
| 'waiting';
|
|
38
|
+
|
|
39
|
+
/** Per-node status — matches run-to-builder-state's `WorkflowNodeStatus`. */
|
|
40
|
+
export type NodeStatus =
|
|
41
|
+
| 'idle'
|
|
42
|
+
| 'pending'
|
|
43
|
+
| 'running'
|
|
44
|
+
| 'success'
|
|
45
|
+
| 'error'
|
|
46
|
+
| 'skipped';
|
|
47
|
+
|
|
48
|
+
export type ExecutionMode = 'manual' | 'trigger' | 'webhook';
|
|
49
|
+
|
|
50
|
+
/** A node placed in a workflow graph (engine-defined shape). */
|
|
51
|
+
export interface WorkflowNode {
|
|
52
|
+
id: string;
|
|
53
|
+
type: string;
|
|
54
|
+
parameters?: Record<string, unknown>;
|
|
55
|
+
position?: [number, number];
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
10
58
|
|
|
11
59
|
export interface Workflow {
|
|
12
60
|
id: string;
|
|
13
61
|
name: string;
|
|
14
62
|
description: string;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
nodes: any[];
|
|
18
|
-
connections: Record<string, any>;
|
|
19
|
-
settings: Record<string, any>;
|
|
20
|
-
staticData: Record<string, any>;
|
|
63
|
+
nodes: WorkflowNode[];
|
|
64
|
+
connections: Record<string, unknown>;
|
|
21
65
|
isActive: boolean;
|
|
22
|
-
version
|
|
23
|
-
isTemplate
|
|
24
|
-
|
|
25
|
-
executionsCount
|
|
26
|
-
createdAt
|
|
27
|
-
updatedAt
|
|
66
|
+
version?: number;
|
|
67
|
+
isTemplate?: boolean;
|
|
68
|
+
settings?: Record<string, unknown>;
|
|
69
|
+
executionsCount?: number;
|
|
70
|
+
createdAt?: string;
|
|
71
|
+
updatedAt?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A registered node type the builder can place. */
|
|
75
|
+
export interface NodeTypeDef {
|
|
76
|
+
slug: string;
|
|
77
|
+
category: string;
|
|
78
|
+
label: string;
|
|
79
|
+
/** JSON-schema-ish description of the node's parameters. */
|
|
80
|
+
parameterSchema: Record<string, unknown>;
|
|
81
|
+
/** Names of the node's output connectors. */
|
|
82
|
+
outputLabels: string[];
|
|
28
83
|
}
|
|
29
84
|
|
|
85
|
+
/** Lightweight execution summary (list rows). */
|
|
30
86
|
export interface WorkflowExecution {
|
|
31
87
|
id: string;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
mode: 'manual' | 'trigger' | 'webhook';
|
|
37
|
-
contextData: Record<string, any>;
|
|
38
|
-
state: Record<string, any>;
|
|
88
|
+
workflowId: string;
|
|
89
|
+
status: RunStatus;
|
|
90
|
+
mode: ExecutionMode;
|
|
91
|
+
source?: string;
|
|
39
92
|
startedAt: string | null;
|
|
40
93
|
completedAt: string | null;
|
|
41
|
-
waitUntil: string | null;
|
|
42
|
-
errorMessage: string | null;
|
|
43
|
-
createdAt: string;
|
|
44
|
-
updatedAt: string;
|
|
45
94
|
}
|
|
46
95
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
96
|
+
/** One node's execution record — matches run-to-builder-state's `RunNodeExecution`. */
|
|
97
|
+
export interface WorkflowNodeExecution {
|
|
98
|
+
nodeId: string;
|
|
99
|
+
nodeName?: string;
|
|
100
|
+
nodeType?: string;
|
|
101
|
+
status: NodeStatus;
|
|
102
|
+
executionOrder?: number;
|
|
103
|
+
output?: Record<string, unknown> | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Full execution detail. Structurally matches present-web's `WorkflowRunDetail`
|
|
107
|
+
* so a response can be passed directly to `runToBuilderState`. */
|
|
108
|
+
export interface WorkflowExecutionDetail {
|
|
109
|
+
status: RunStatus;
|
|
110
|
+
state: Record<string, unknown>;
|
|
111
|
+
nodeExecutions: WorkflowNodeExecution[];
|
|
112
|
+
contextData?: Record<string, unknown>;
|
|
54
113
|
}
|
|
55
114
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
115
|
+
/** The contract returned by POST execute/retry: carries the executionId. */
|
|
116
|
+
export interface ExecuteWorkflowResponse {
|
|
117
|
+
executionId: string;
|
|
118
|
+
status: RunStatus;
|
|
59
119
|
}
|
|
60
120
|
|
|
61
|
-
export interface
|
|
121
|
+
export interface WorkflowInput {
|
|
62
122
|
name: string;
|
|
63
123
|
description?: string;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
connections?: Record<string, any>;
|
|
67
|
-
settings?: Record<string, any>;
|
|
124
|
+
nodes?: WorkflowNode[];
|
|
125
|
+
connections?: Record<string, unknown>;
|
|
68
126
|
isActive?: boolean;
|
|
127
|
+
settings?: Record<string, unknown>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface WorkflowListParams {
|
|
131
|
+
page?: number;
|
|
132
|
+
pageSize?: number;
|
|
133
|
+
search?: string;
|
|
134
|
+
ordering?: string;
|
|
135
|
+
isActive?: boolean;
|
|
136
|
+
isTemplate?: boolean;
|
|
137
|
+
[key: string]: unknown;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface ExecutionListParams {
|
|
141
|
+
page?: number;
|
|
142
|
+
pageSize?: number;
|
|
143
|
+
status?: RunStatus;
|
|
144
|
+
ordering?: string;
|
|
145
|
+
[key: string]: unknown;
|
|
69
146
|
}
|
|
70
147
|
|
|
71
148
|
export class WorkflowsApi {
|
|
72
149
|
constructor(private client: ApiClient) {}
|
|
73
150
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
async list(filters?: WorkflowFilters) {
|
|
78
|
-
const params = new URLSearchParams();
|
|
79
|
-
|
|
80
|
-
if (filters?.team) params.append('team', filters.team);
|
|
81
|
-
if (filters?.isActive !== undefined) params.append('isActive', String(filters.isActive));
|
|
82
|
-
if (filters?.isTemplate !== undefined) params.append('isTemplate', String(filters.isTemplate));
|
|
83
|
-
if (filters?.page) params.append('page', String(filters.page));
|
|
84
|
-
if (filters?.pageSize) params.append('pageSize', String(filters.pageSize));
|
|
85
|
-
if (filters?.ordering) params.append('ordering', filters.ordering);
|
|
86
|
-
|
|
87
|
-
return this.client.get<{ results: Workflow[]; count: number; next: string | null; previous: string | null }>(
|
|
88
|
-
`/api/v1/workflows/?${params.toString()}`
|
|
89
|
-
);
|
|
151
|
+
// --- Workflows ---
|
|
152
|
+
listWorkflows(params?: WorkflowListParams): Promise<PaginatedResponse<Workflow>> {
|
|
153
|
+
return this.client.fetch.get<PaginatedResponse<Workflow>>(E.workflows, { params });
|
|
90
154
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* Get workflow by ID
|
|
94
|
-
*/
|
|
95
|
-
async get(id: string) {
|
|
96
|
-
return this.client.get<Workflow>(`/api/v1/workflows/${id}/`);
|
|
155
|
+
getWorkflow(id: string): Promise<Workflow> {
|
|
156
|
+
return this.client.fetch.get<Workflow>(E.workflow(id));
|
|
97
157
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
* Create a new workflow
|
|
101
|
-
*/
|
|
102
|
-
async create(data: CreateWorkflowInput) {
|
|
103
|
-
return this.client.post<Workflow>('/api/v1/workflows/', data);
|
|
158
|
+
createWorkflow(data: WorkflowInput): Promise<Workflow> {
|
|
159
|
+
return this.client.fetch.post<Workflow>(E.workflows, data);
|
|
104
160
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* Update workflow
|
|
108
|
-
*/
|
|
109
|
-
async update(id: string, data: Partial<CreateWorkflowInput>) {
|
|
110
|
-
return this.client.patch<Workflow>(`/api/v1/workflows/${id}/`, data);
|
|
161
|
+
updateWorkflow(id: string, data: Partial<WorkflowInput>): Promise<Workflow> {
|
|
162
|
+
return this.client.fetch.patch<Workflow>(E.workflow(id), data);
|
|
111
163
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* Delete workflow
|
|
115
|
-
*/
|
|
116
|
-
async delete(id: string) {
|
|
117
|
-
return this.client.delete(`/api/v1/workflows/${id}/`);
|
|
164
|
+
deleteWorkflow(id: string): Promise<void> {
|
|
165
|
+
return this.client.fetch.delete<void>(E.workflow(id));
|
|
118
166
|
}
|
|
119
167
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
async execute(id: string, input?: ExecuteWorkflowInput) {
|
|
124
|
-
return this.client.post<WorkflowExecution>(
|
|
125
|
-
`/api/v1/workflows/${id}/execute/`,
|
|
126
|
-
input || {}
|
|
127
|
-
);
|
|
168
|
+
// --- Node type registry ---
|
|
169
|
+
getNodeTypes(): Promise<NodeTypeDef[]> {
|
|
170
|
+
return this.client.fetch.get<NodeTypeDef[]>(E.nodeTypes);
|
|
128
171
|
}
|
|
129
172
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return this.client.get<Workflow[]>('/api/v1/workflows/templates/');
|
|
173
|
+
// --- Execution ---
|
|
174
|
+
/** Kick off a run. The response carries the executionId (the contract). */
|
|
175
|
+
executeWorkflow(id: string, contextData?: Record<string, unknown>): Promise<ExecuteWorkflowResponse> {
|
|
176
|
+
return this.client.fetch.post<ExecuteWorkflowResponse>(E.execute(id), { contextData });
|
|
135
177
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
178
|
+
getExecution(executionId: string): Promise<WorkflowExecutionDetail> {
|
|
179
|
+
return this.client.fetch.get<WorkflowExecutionDetail>(E.execution(executionId));
|
|
180
|
+
}
|
|
181
|
+
listExecutions(
|
|
182
|
+
workflowId: string,
|
|
183
|
+
params?: ExecutionListParams,
|
|
184
|
+
): Promise<PaginatedResponse<WorkflowExecution>> {
|
|
185
|
+
return this.client.fetch.get<PaginatedResponse<WorkflowExecution>>(E.executions(workflowId), { params });
|
|
186
|
+
}
|
|
187
|
+
cancelExecution(executionId: string): Promise<WorkflowExecution> {
|
|
188
|
+
return this.client.fetch.post<WorkflowExecution>(E.cancel(executionId), undefined);
|
|
189
|
+
}
|
|
190
|
+
/** Re-run a failed/cancelled execution; response carries the new executionId. */
|
|
191
|
+
retryExecution(executionId: string): Promise<ExecuteWorkflowResponse> {
|
|
192
|
+
return this.client.fetch.post<ExecuteWorkflowResponse>(E.retry(executionId), undefined);
|
|
151
193
|
}
|
|
152
194
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
195
|
+
// --- Lifecycle ---
|
|
196
|
+
cloneWorkflow(id: string): Promise<Workflow> {
|
|
197
|
+
return this.client.fetch.post<Workflow>(E.clone(id), undefined);
|
|
198
|
+
}
|
|
199
|
+
publishWorkflow(id: string): Promise<Workflow> {
|
|
200
|
+
return this.client.fetch.post<Workflow>(E.publish(id), undefined);
|
|
201
|
+
}
|
|
202
|
+
getVersions(id: string): Promise<Workflow[]> {
|
|
203
|
+
return this.client.fetch.get<Workflow[]>(E.versions(id));
|
|
158
204
|
}
|
|
159
205
|
}
|