Skip to content

Commit a163924

Browse files
committed
Fix frontend API v2 contracts
1 parent 4527dc1 commit a163924

14 files changed

Lines changed: 262 additions & 110 deletions

frontend/src/lib/api/actions.js

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,75 @@
1-
import { fetchAPI } from './core.js';
1+
import { fetchV2Data } from './core.js';
2+
3+
function actionMutationData(data) {
4+
const {
5+
name,
6+
description,
7+
trigger_type,
8+
trigger_config,
9+
is_enabled,
10+
actor_user_id,
11+
allowed_role_ids,
12+
nodes,
13+
edges,
14+
} = data;
15+
return {
16+
name,
17+
description,
18+
trigger_type,
19+
trigger_config,
20+
is_enabled,
21+
actor_user_id,
22+
allowed_role_ids,
23+
nodes,
24+
edges,
25+
};
26+
}
227

328
export const actions = {
429
// getCatalog returns the workspace-scoped action catalog: every available
530
// trigger and node type with its JSON-Schema config, plus the action
631
// capabilities reachable from this workspace. The visual palette is
732
// built from this rather than a hardcoded list so adding a node type
833
// server-side automatically surfaces it in the editor.
9-
getCatalog: (workspaceId) => fetchAPI(`/workspaces/${workspaceId}/action-catalog`),
34+
getCatalog: (workspaceId) => fetchV2Data(`/workspaces/${workspaceId}/action-catalog`),
1035
getAll: (workspaceId, requestOptions = {}) =>
11-
fetchAPI(`/workspaces/${workspaceId}/actions`, requestOptions),
12-
get: (workspaceId, id) => fetchAPI(`/workspaces/${workspaceId}/actions/${id}`),
36+
fetchV2Data(`/workspaces/${workspaceId}/actions`, requestOptions),
37+
get: (workspaceId, id) => fetchV2Data(`/workspaces/${workspaceId}/actions/${id}`),
1338
create: (workspaceId, data) =>
14-
fetchAPI(`/workspaces/${workspaceId}/actions`, {
39+
fetchV2Data(`/workspaces/${workspaceId}/actions`, {
1540
method: 'POST',
16-
body: JSON.stringify(data),
41+
body: JSON.stringify(actionMutationData(data)),
1742
}),
1843
update: (workspaceId, id, data) =>
19-
fetchAPI(`/workspaces/${workspaceId}/actions/${id}`, {
20-
method: 'PUT',
21-
body: JSON.stringify(data),
44+
fetchV2Data(`/workspaces/${workspaceId}/actions/${id}`, {
45+
method: 'PATCH',
46+
headers: { 'Content-Type': 'application/merge-patch+json' },
47+
body: JSON.stringify(actionMutationData(data)),
2248
}),
2349
delete: (workspaceId, id) =>
24-
fetchAPI(`/workspaces/${workspaceId}/actions/${id}`, {
50+
fetchV2Data(`/workspaces/${workspaceId}/actions/${id}`, {
2551
method: 'DELETE',
2652
}),
53+
toggle: (workspaceId, id, isEnabled) =>
54+
fetchV2Data(`/workspaces/${workspaceId}/actions/${id}/toggle`, {
55+
method: 'POST',
56+
body: JSON.stringify({ is_enabled: isEnabled }),
57+
}),
2758
execute: (workspaceId, actionId, itemId) =>
28-
fetchAPI(`/workspaces/${workspaceId}/actions/${actionId}/execute`, {
59+
fetchV2Data(`/workspaces/${workspaceId}/actions/${actionId}/execute`, {
2960
method: 'POST',
3061
body: JSON.stringify({ item_id: itemId }),
3162
}),
3263
getLogs: (workspaceId, actionId) =>
33-
fetchAPI(`/workspaces/${workspaceId}/actions/${actionId}/logs`),
64+
fetchV2Data(`/workspaces/${workspaceId}/actions/${actionId}/logs`),
3465
};
3566

3667
// Action templates: read-only registry shipped with the binary, plus
3768
// instantiation into a workspace via snapshot copy.
3869
export const actionTemplates = {
39-
list: () => fetchAPI('/action-templates'),
70+
list: () => fetchV2Data('/action-templates'),
4071
apply: (workspaceId, templateKey) =>
41-
fetchAPI(`/workspaces/${workspaceId}/action-templates/${templateKey}/apply`, {
72+
fetchV2Data(`/workspaces/${workspaceId}/action-templates/${templateKey}/apply`, {
4273
method: 'POST',
4374
}),
4475
};

frontend/src/lib/api/core.js

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88

99
// Use relative path for API calls - Vite proxy will handle dev, production uses same origin
1010
export const API_BASE = '/api';
11+
export const API_V2_BASE = '/api/v2';
1112
export const ADMIN_UI_MUTATION_EVENT = 'windshift:admin-ui-mutation';
1213

1314
// Ensure the clock-drift warning toast fires at most once per session
@@ -54,7 +55,7 @@ function normalizeGETEndpoint(endpoint) {
5455
}
5556
}
5657

57-
function inFlightGETKey(endpoint, options) {
58+
function inFlightGETKey(base, endpoint, options) {
5859
if (!apiRequestSessionKey) return null;
5960
const method = String(options?.method || 'GET').toUpperCase();
6061
if (method !== 'GET') return null;
@@ -64,7 +65,7 @@ function inFlightGETKey(endpoint, options) {
6465
const optionKeys = Object.keys(options || {});
6566
if (optionKeys.some((key) => key !== 'method')) return null;
6667

67-
return `${apiRequestSessionKey}|${requestLocale()}|${normalizeGETEndpoint(endpoint)}`;
68+
return `${apiRequestSessionKey}|${requestLocale()}|${base}|${normalizeGETEndpoint(endpoint)}`;
6869
}
6970

7071
function isAdminUIPath() {
@@ -110,9 +111,11 @@ function createApiError(response, responseText) {
110111
// Try to parse structured error from response
111112
try {
112113
const parsed = JSON.parse(responseText);
113-
/** @type {any} */ (error).code = parsed.code;
114-
/** @type {any} */ (error).errorCode = parsed.code; // Alias for compatibility
115-
/** @type {any} */ (error).details = parsed.details || {};
114+
const payload =
115+
typeof parsed.error === 'object' && parsed.error !== null ? parsed.error : parsed;
116+
/** @type {any} */ (error).code = payload.code;
117+
/** @type {any} */ (error).errorCode = payload.code; // Alias for compatibility
118+
/** @type {any} */ (error).details = payload.details || {};
116119
/** @type {any} */ (error).requestId = parsed.request_id;
117120
/** @type {any} */ (error).body = parsed;
118121
// Authentication policy responses carry flow-control fields alongside the
@@ -122,7 +125,8 @@ function createApiError(response, responseText) {
122125
/** @type {any} */ (error).enrollment_required = parsed.enrollment_required === true;
123126
/** @type {any} */ (error).sso_required = parsed.sso_required === true;
124127
/** @type {any} */ (error).policy_message = parsed.policy_message;
125-
error.message = parsed.error || parsed.message || error.message;
128+
error.message =
129+
(typeof parsed.error === 'string' ? parsed.error : payload.message) || error.message;
126130
} catch {
127131
// Response is not JSON, keep original message
128132
}
@@ -135,10 +139,11 @@ function createApiError(response, responseText) {
135139
}
136140

137141
/**
142+
* @param {string} base
138143
* @param {string} endpoint
139144
* @param {RequestInit & { timeout?: number }} [options]
140145
*/
141-
async function performFetchAPI(endpoint, options = {}) {
146+
async function performFetchAPI(base, endpoint, options = {}) {
142147
const { timeout: requestedTimeout = 0, signal: callerSignal, ...fetchOptions } = options;
143148
const isFormData = typeof FormData !== 'undefined' && fetchOptions.body instanceof FormData;
144149
const headers = isFormData
@@ -187,7 +192,7 @@ async function performFetchAPI(endpoint, options = {}) {
187192

188193
let response;
189194
try {
190-
response = await fetch(`${API_BASE}${endpoint}`, {
195+
response = await fetch(`${base}${endpoint}`, {
191196
...fetchOptions,
192197
credentials: 'same-origin', // Include cookies for session auth
193198
headers,
@@ -301,14 +306,43 @@ async function performFetchAPI(endpoint, options = {}) {
301306
}
302307

303308
export function fetchAPI(endpoint, options = {}) {
304-
const key = inFlightGETKey(endpoint, options);
305-
if (!key) return performFetchAPI(endpoint, options);
309+
return fetchFromAPI(API_BASE, endpoint, options);
310+
}
311+
312+
export function fetchAPIV2(endpoint, options = {}) {
313+
return fetchFromAPI(API_V2_BASE, endpoint, options);
314+
}
315+
316+
export async function fetchV2Data(endpoint, options = {}) {
317+
const document = await fetchAPIV2(endpoint, options);
318+
return document?.data;
319+
}
320+
321+
export async function fetchAllV2Pages(endpoint, options = {}) {
322+
const url = new URL(endpoint, 'https://windshift.invalid');
323+
url.searchParams.set('page_size', '100');
324+
const items = [];
325+
let page = 1;
326+
let totalPages = 1;
327+
do {
328+
url.searchParams.set('page', String(page));
329+
const document = await fetchAPIV2(`${url.pathname}${url.search}`, options);
330+
items.push(...(document?.data ?? []));
331+
totalPages = document?.pagination?.total_pages ?? 0;
332+
page += 1;
333+
} while (page <= totalPages);
334+
return items;
335+
}
336+
337+
function fetchFromAPI(base, endpoint, options) {
338+
const key = inFlightGETKey(base, endpoint, options);
339+
if (!key) return performFetchAPI(base, endpoint, options);
306340

307341
const existing = inFlightGetRequests.get(key);
308342
if (existing) return existing;
309343

310344
let trackedRequest;
311-
trackedRequest = performFetchAPI(endpoint, options).finally(() => {
345+
trackedRequest = performFetchAPI(base, endpoint, options).finally(() => {
312346
if (inFlightGetRequests.get(key) === trackedRequest) {
313347
inFlightGetRequests.delete(key);
314348
}

frontend/src/lib/api/createCrudClient.js

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1-
import { fetchAPI } from './core.js';
1+
import { fetchAllV2Pages, fetchAPI, fetchV2Data } from './core.js';
22
import { buildQueryString } from './utils.js';
33

44
/** Build CRUD clients with parent-scoped, flat-item, or admin-write paths. */
55
export function createCrudClient(basePath, options = {}) {
6-
const { parentPath, itemPath, adminBasePath } = options;
6+
const {
7+
parentPath,
8+
itemPath,
9+
adminBasePath,
10+
readV2 = false,
11+
v2 = false,
12+
allV2 = false,
13+
} = options;
14+
const detailRead = readV2 || v2 || allV2 ? fetchV2Data : fetchAPI;
15+
const listRead = allV2 ? fetchAllV2Pages : detailRead;
16+
const write = v2 ? fetchV2Data : fetchAPI;
17+
const updateMethod = v2 ? 'PATCH' : 'PUT';
18+
const updateHeaders = v2 ? { 'Content-Type': 'application/merge-patch+json' } : undefined;
719

820
if (parentPath) {
921
const collection = (parentId) => `${parentPath}/${parentId}${basePath}`;
@@ -13,20 +25,21 @@ export function createCrudClient(basePath, options = {}) {
1325
const item = (id) => `${itemPath}/${id}`;
1426
return {
1527
getAll: (parentId, filters = {}, requestOptions = {}) =>
16-
fetchAPI(`${collection(parentId)}${buildQueryString(filters)}`, requestOptions),
17-
get: (id, requestOptions = {}) => fetchAPI(item(id), requestOptions),
28+
listRead(`${collection(parentId)}${buildQueryString(filters)}`, requestOptions),
29+
get: (id, requestOptions = {}) => detailRead(item(id), requestOptions),
1830
create: (parentId, data) =>
19-
fetchAPI(collection(parentId), {
31+
write(collection(parentId), {
2032
method: 'POST',
2133
body: JSON.stringify(data),
2234
}),
2335
update: (id, data) =>
24-
fetchAPI(item(id), {
25-
method: 'PUT',
36+
write(item(id), {
37+
method: updateMethod,
38+
headers: updateHeaders,
2639
body: JSON.stringify(data),
2740
}),
2841
delete: (id) =>
29-
fetchAPI(item(id), {
42+
write(item(id), {
3043
method: 'DELETE',
3144
}),
3245
};
@@ -36,20 +49,21 @@ export function createCrudClient(basePath, options = {}) {
3649
const item = (parentId, id) => `${collection(parentId)}/${id}`;
3750
return {
3851
getAll: (parentId, filters = {}, requestOptions = {}) =>
39-
fetchAPI(`${collection(parentId)}${buildQueryString(filters)}`, requestOptions),
40-
get: (parentId, id, requestOptions = {}) => fetchAPI(item(parentId, id), requestOptions),
52+
listRead(`${collection(parentId)}${buildQueryString(filters)}`, requestOptions),
53+
get: (parentId, id, requestOptions = {}) => detailRead(item(parentId, id), requestOptions),
4154
create: (parentId, data) =>
42-
fetchAPI(collection(parentId), {
55+
write(collection(parentId), {
4356
method: 'POST',
4457
body: JSON.stringify(data),
4558
}),
4659
update: (parentId, id, data) =>
47-
fetchAPI(item(parentId, id), {
48-
method: 'PUT',
60+
write(item(parentId, id), {
61+
method: updateMethod,
62+
headers: updateHeaders,
4963
body: JSON.stringify(data),
5064
}),
5165
delete: (parentId, id) =>
52-
fetchAPI(item(parentId, id), {
66+
write(item(parentId, id), {
5367
method: 'DELETE',
5468
}),
5569
};
@@ -58,20 +72,21 @@ export function createCrudClient(basePath, options = {}) {
5872
const writePath = adminBasePath ?? basePath;
5973
return {
6074
getAll: (filters = {}, requestOptions = {}) =>
61-
fetchAPI(`${basePath}${buildQueryString(filters)}`, requestOptions),
62-
get: (id, requestOptions = {}) => fetchAPI(`${basePath}/${id}`, requestOptions),
75+
listRead(`${basePath}${buildQueryString(filters)}`, requestOptions),
76+
get: (id, requestOptions = {}) => detailRead(`${basePath}/${id}`, requestOptions),
6377
create: (data) =>
64-
fetchAPI(writePath, {
78+
write(writePath, {
6579
method: 'POST',
6680
body: JSON.stringify(data),
6781
}),
6882
update: (id, data) =>
69-
fetchAPI(`${writePath}/${id}`, {
70-
method: 'PUT',
83+
write(`${writePath}/${id}`, {
84+
method: updateMethod,
85+
headers: updateHeaders,
7186
body: JSON.stringify(data),
7287
}),
7388
delete: (id) =>
74-
fetchAPI(`${writePath}/${id}`, {
89+
write(`${writePath}/${id}`, {
7590
method: 'DELETE',
7691
}),
7792
};

0 commit comments

Comments
 (0)