@shipfox/client-secrets 5.0.0 → 6.0.1

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.
Files changed (72) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +38 -0
  3. package/dist/components/form-errors.d.ts.map +1 -1
  4. package/dist/components/form-errors.js +0 -6
  5. package/dist/components/form-errors.js.map +1 -1
  6. package/dist/components/secret-form.d.ts.map +1 -1
  7. package/dist/components/secret-form.js +6 -8
  8. package/dist/components/secret-form.js.map +1 -1
  9. package/dist/components/variable-form.d.ts.map +1 -1
  10. package/dist/components/variable-form.js +6 -8
  11. package/dist/components/variable-form.js.map +1 -1
  12. package/dist/components/workspace-secrets-section.d.ts.map +1 -1
  13. package/dist/components/workspace-secrets-section.js +4 -2
  14. package/dist/components/workspace-secrets-section.js.map +1 -1
  15. package/dist/components/workspace-secrets-section.stories.d.ts.map +1 -1
  16. package/dist/components/workspace-secrets-section.stories.js.map +1 -1
  17. package/dist/components/workspace-variables-section.d.ts.map +1 -1
  18. package/dist/components/workspace-variables-section.js +5 -3
  19. package/dist/components/workspace-variables-section.js.map +1 -1
  20. package/dist/components/workspace-variables-section.stories.d.ts.map +1 -1
  21. package/dist/components/workspace-variables-section.stories.js.map +1 -1
  22. package/dist/core/store.d.ts +54 -0
  23. package/dist/core/store.d.ts.map +1 -0
  24. package/dist/core/store.js +44 -0
  25. package/dist/core/store.js.map +1 -0
  26. package/dist/feature.d.ts +13 -0
  27. package/dist/feature.d.ts.map +1 -1
  28. package/dist/feature.js +17 -16
  29. package/dist/feature.js.map +1 -1
  30. package/dist/hooks/api/create-store-api.d.ts +31 -49
  31. package/dist/hooks/api/create-store-api.d.ts.map +1 -1
  32. package/dist/hooks/api/create-store-api.js +44 -45
  33. package/dist/hooks/api/create-store-api.js.map +1 -1
  34. package/dist/hooks/api/secrets.d.ts +16 -55
  35. package/dist/hooks/api/secrets.d.ts.map +1 -1
  36. package/dist/hooks/api/secrets.js +10 -2
  37. package/dist/hooks/api/secrets.js.map +1 -1
  38. package/dist/hooks/api/store-mapper.d.ts +7 -0
  39. package/dist/hooks/api/store-mapper.d.ts.map +1 -0
  40. package/dist/hooks/api/store-mapper.js +36 -0
  41. package/dist/hooks/api/store-mapper.js.map +1 -0
  42. package/dist/hooks/api/variables.d.ts +25 -78
  43. package/dist/hooks/api/variables.d.ts.map +1 -1
  44. package/dist/hooks/api/variables.js +37 -28
  45. package/dist/hooks/api/variables.js.map +1 -1
  46. package/dist/tsconfig.test.tsbuildinfo +1 -1
  47. package/package.json +9 -10
  48. package/src/components/form-errors.test.ts +2 -2
  49. package/src/components/form-errors.ts +0 -3
  50. package/src/components/secret-form.tsx +15 -5
  51. package/src/components/variable-form.tsx +15 -5
  52. package/src/components/workspace-secrets-section.stories.tsx +1 -2
  53. package/src/components/workspace-secrets-section.tsx +8 -4
  54. package/src/components/workspace-variables-section.stories.tsx +1 -2
  55. package/src/components/workspace-variables-section.tsx +10 -6
  56. package/src/core/store.test.ts +38 -0
  57. package/src/core/store.ts +91 -0
  58. package/src/feature.ts +13 -11
  59. package/src/hooks/api/create-store-api.ts +104 -75
  60. package/src/hooks/api/secrets.test.ts +16 -10
  61. package/src/hooks/api/secrets.ts +11 -14
  62. package/src/hooks/api/store-mapper.test.ts +39 -0
  63. package/src/hooks/api/store-mapper.ts +47 -0
  64. package/src/hooks/api/variables.ts +52 -36
  65. package/tsconfig.build.tsbuildinfo +1 -1
  66. package/vercel.json +5 -0
  67. package/dist/components/store-key.d.ts +0 -20
  68. package/dist/components/store-key.d.ts.map +0 -1
  69. package/dist/components/store-key.js +0 -25
  70. package/dist/components/store-key.js.map +0 -1
  71. package/src/components/store-key.test.ts +0 -53
  72. package/src/components/store-key.ts +0 -36
@@ -1,102 +1,124 @@
1
- import {SECRETS_MAX_LIST_LIMIT, type SecretWriteWarningDto} from '@shipfox/api-secrets-dto';
2
- import {apiRequest} from '@shipfox/client-api';
3
- import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
1
+ import {SECRETS_MAX_LIST_LIMIT} from '@shipfox/api-secrets-dto';
2
+ import {
3
+ checkedApiRequest,
4
+ emptyResponseSchema,
5
+ type StandardSchema,
6
+ type StandardSchemaOutput,
7
+ } from '@shipfox/client-api';
8
+ import {
9
+ type FetchQueryOptions,
10
+ queryOptions,
11
+ useMutation,
12
+ useQuery,
13
+ useQueryClient,
14
+ } from '@tanstack/react-query';
15
+ import {
16
+ type DeleteStoreCommand,
17
+ type PutStoreCommand,
18
+ projectIdFromScope,
19
+ type StoreScope,
20
+ type StoreWriteWarning,
21
+ workspaceStoreScope,
22
+ } from '#core/store.js';
4
23
 
5
- /**
6
- * Secrets and variables share an identical management surface (list / put /
7
- * delete under `/workspaces/:id/{resource}`); only the response envelope keys
8
- * and DTO types differ. This factory builds the typed transport functions,
9
- * query keys, and React Query hooks for one resource so `secrets.ts` and
10
- * `variables.ts` stay thin instantiations. The forms and sections diverge
11
- * (write-only vs readable) and are kept separate on purpose.
12
- */
13
- export interface CreateStoreApiConfig<TItem, TListResponse, TPutResponse> {
14
- /** URL segment and query-key namespace, e.g. `'secrets'` or `'variables'`. */
15
- resource: string;
16
- /** Pulls the item array out of a list response. */
17
- listItems: (response: TListResponse) => TItem[];
18
- /** Pulls the single item out of a put response. */
19
- putItem: (response: TPutResponse) => TItem;
20
- }
21
-
22
- export interface PutResult<TItem> {
23
- item: TItem;
24
- warnings: SecretWriteWarningDto[];
25
- }
24
+ type StoreApiConfig<
25
+ TListItem,
26
+ TPutItem,
27
+ TListSchema extends StandardSchema,
28
+ TPutSchema extends StandardSchema,
29
+ > = {
30
+ resource: 'secrets' | 'variables';
31
+ listResponseSchema: TListSchema;
32
+ putResponseSchema: TPutSchema;
33
+ toListItems: (response: StandardSchemaOutput<TListSchema>) => TListItem[];
34
+ toPutResult: (response: StandardSchemaOutput<TPutSchema>) => {
35
+ item: TPutItem;
36
+ warnings: StoreWriteWarning[];
37
+ };
38
+ };
26
39
 
27
40
  export function createStoreApi<
28
- TItem,
29
- TListResponse,
30
- TPutBody,
31
- TPutResponse extends {warnings: SecretWriteWarningDto[]},
32
- >(config: CreateStoreApiConfig<TItem, TListResponse, TPutResponse>) {
33
- const {resource, listItems, putItem} = config;
34
-
41
+ TListItem,
42
+ TPutItem,
43
+ TListSchema extends StandardSchema,
44
+ TPutSchema extends StandardSchema,
45
+ >(config: StoreApiConfig<TListItem, TPutItem, TListSchema, TPutSchema>) {
46
+ const {resource} = config;
35
47
  const queryKeys = {
36
48
  all: [resource] as const,
37
- list: (workspaceId: string) => [resource, 'list', workspaceId] as const,
49
+ list: (workspaceId: string, scope: StoreScope = workspaceStoreScope) =>
50
+ [...queryKeys.all, 'list', workspaceId, scope] as const,
38
51
  };
39
52
 
40
- // The store is a bounded set (per-workspace cap), so the UI fetches every key
41
- // in one call rather than paginating: request the max limit and ignore the
42
- // cursor. See SECRETS_MAX_LIST_LIMIT / the S1a list route.
53
+ type ListQueryOptions = FetchQueryOptions<
54
+ TListItem[],
55
+ Error,
56
+ TListItem[],
57
+ ReturnType<typeof queryKeys.list>
58
+ >;
59
+
60
+ function searchForScope(scope: StoreScope, includeLimit = false): string {
61
+ const search = new URLSearchParams();
62
+ if (includeLimit) search.set('limit', String(SECRETS_MAX_LIST_LIMIT));
63
+ const projectId = projectIdFromScope(scope);
64
+ if (projectId) search.set('project_id', projectId);
65
+ return search.toString();
66
+ }
67
+
43
68
  async function listAll(params: {
44
69
  workspaceId: string;
45
- projectId?: string | undefined;
70
+ scope?: StoreScope;
46
71
  signal?: AbortSignal | undefined;
47
- }): Promise<TItem[]> {
48
- const search = new URLSearchParams({limit: String(SECRETS_MAX_LIST_LIMIT)});
49
- if (params.projectId) search.set('project_id', params.projectId);
50
- const response = await apiRequest<TListResponse>(
51
- `/workspaces/${params.workspaceId}/${resource}?${search.toString()}`,
72
+ }): Promise<TListItem[]> {
73
+ const scope = params.scope ?? workspaceStoreScope;
74
+ const response = await checkedApiRequest(
75
+ config.listResponseSchema,
76
+ `/workspaces/${params.workspaceId}/${resource}?${searchForScope(scope, true)}`,
52
77
  {signal: params.signal},
53
78
  );
54
- return listItems(response);
79
+ return config.toListItems(response);
55
80
  }
56
81
 
57
- async function put(params: {
58
- workspaceId: string;
59
- key: string;
60
- body: TPutBody;
61
- }): Promise<PutResult<TItem>> {
62
- const response = await apiRequest<TPutResponse>(
63
- `/workspaces/${params.workspaceId}/${resource}/${encodeURIComponent(params.key)}`,
64
- {method: 'PUT', body: params.body},
82
+ async function put(command: PutStoreCommand) {
83
+ const response = await checkedApiRequest(
84
+ config.putResponseSchema,
85
+ `/workspaces/${command.workspaceId}/${resource}/${encodeURIComponent(command.key)}`,
86
+ {
87
+ method: 'PUT',
88
+ body: {value: command.value, project_id: projectIdFromScope(command.scope)},
89
+ },
65
90
  );
66
- return {item: putItem(response), warnings: response.warnings};
91
+ return config.toPutResult(response);
67
92
  }
68
93
 
69
- async function remove(params: {
70
- workspaceId: string;
71
- key: string;
72
- projectId?: string | undefined;
73
- }): Promise<void> {
74
- const search = params.projectId
75
- ? `?${new URLSearchParams({project_id: params.projectId}).toString()}`
76
- : '';
77
- await apiRequest<void>(
78
- `/workspaces/${params.workspaceId}/${resource}/${encodeURIComponent(params.key)}${search}`,
94
+ async function remove(command: DeleteStoreCommand): Promise<void> {
95
+ const search = searchForScope(command.scope);
96
+ await checkedApiRequest(
97
+ emptyResponseSchema,
98
+ `/workspaces/${command.workspaceId}/${resource}/${encodeURIComponent(command.key)}${search ? `?${search}` : ''}`,
79
99
  {method: 'DELETE'},
80
100
  );
81
101
  }
82
102
 
83
- function useListQuery(workspaceId: string | undefined) {
84
- return useQuery({
85
- queryKey: workspaceId ? queryKeys.list(workspaceId) : [resource, 'list'],
86
- enabled: Boolean(workspaceId),
87
- queryFn: ({signal}) => listAll({workspaceId: workspaceId ?? '', signal}),
103
+ function listQueryOptions(
104
+ workspaceId: string,
105
+ scope: StoreScope = workspaceStoreScope,
106
+ ): ListQueryOptions {
107
+ return queryOptions({
108
+ queryKey: queryKeys.list(workspaceId, scope),
109
+ queryFn: ({signal}) => listAll({workspaceId, scope, signal}),
88
110
  });
89
111
  }
90
112
 
113
+ function useListQuery(workspaceId: string | undefined, scope: StoreScope = workspaceStoreScope) {
114
+ return useQuery({...listQueryOptions(workspaceId ?? '', scope), enabled: Boolean(workspaceId)});
115
+ }
116
+
91
117
  function usePutMutation() {
92
118
  const queryClient = useQueryClient();
93
119
  return useMutation({
94
120
  mutationFn: put,
95
- // Invalidate the whole resource namespace so the per-item detail cache
96
- // (full variable values) is refreshed too, not just the list.
97
- onSuccess: async () => {
98
- await queryClient.invalidateQueries({queryKey: queryKeys.all});
99
- },
121
+ onSuccess: async () => await queryClient.invalidateQueries({queryKey: queryKeys.all}),
100
122
  });
101
123
  }
102
124
 
@@ -104,11 +126,18 @@ export function createStoreApi<
104
126
  const queryClient = useQueryClient();
105
127
  return useMutation({
106
128
  mutationFn: remove,
107
- onSuccess: async () => {
108
- await queryClient.invalidateQueries({queryKey: queryKeys.all});
109
- },
129
+ onSuccess: async () => await queryClient.invalidateQueries({queryKey: queryKeys.all}),
110
130
  });
111
131
  }
112
132
 
113
- return {queryKeys, listAll, put, remove, useListQuery, usePutMutation, useDeleteMutation};
133
+ return {
134
+ queryKeys,
135
+ listAll,
136
+ put,
137
+ remove,
138
+ listQueryOptions,
139
+ useListQuery,
140
+ usePutMutation,
141
+ useDeleteMutation,
142
+ };
114
143
  }
@@ -1,11 +1,11 @@
1
1
  import {SECRETS_MAX_LIST_LIMIT} from '@shipfox/api-secrets-dto';
2
2
  import {configureApiClient} from '@shipfox/client-api';
3
+ import {workspaceStoreScope} from '#core/store.js';
3
4
  import {
4
5
  SECRETS_TEST_WORKSPACE_ID,
5
6
  secret,
6
7
  secretsListResponse,
7
8
  variable,
8
- variableListItem,
9
9
  variablesListResponse,
10
10
  } from '#test/fixtures/secrets.js';
11
11
  import {deleteSecret, listSecrets, putSecret} from './secrets.js';
@@ -32,7 +32,7 @@ describe('store transport', () => {
32
32
 
33
33
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
34
34
  expect(fetchImpl).toHaveBeenCalledTimes(1);
35
- expect(result).toEqual([secret()]);
35
+ expect(result).toMatchObject([{key: 'MY_TOKEN', scope: {kind: 'workspace'}}]);
36
36
  expect(request.method).toBe('GET');
37
37
  expect(request.url).toBe(
38
38
  `https://api.example.test/workspaces/${SECRETS_TEST_WORKSPACE_ID}/secrets?limit=${SECRETS_MAX_LIST_LIMIT}`,
@@ -64,7 +64,8 @@ describe('store transport', () => {
64
64
  const result = await putSecret({
65
65
  workspaceId: SECRETS_TEST_WORKSPACE_ID,
66
66
  key: 'MY_TOKEN',
67
- body: {value: 'short'},
67
+ value: 'short',
68
+ scope: workspaceStoreScope,
68
69
  });
69
70
 
70
71
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
@@ -74,14 +75,18 @@ describe('store transport', () => {
74
75
  );
75
76
  expect(requestBody).toEqual({value: 'short'});
76
77
  expect(result.warnings).toEqual([{code: 'short-secret-value', key: 'MY_TOKEN'}]);
77
- expect(result.item.key).toBe('MY_TOKEN');
78
+ expect(result.item).toMatchObject({key: 'MY_TOKEN', scope: {kind: 'workspace'}});
78
79
  });
79
80
 
80
81
  test('deletes a secret with a 204 and no project scope by default', async () => {
81
82
  const fetchImpl = vi.fn().mockResolvedValue(new Response(null, {status: 204}));
82
83
  configureApiClient({fetchImpl});
83
84
 
84
- const result = await deleteSecret({workspaceId: SECRETS_TEST_WORKSPACE_ID, key: 'MY_TOKEN'});
85
+ const result = await deleteSecret({
86
+ workspaceId: SECRETS_TEST_WORKSPACE_ID,
87
+ key: 'MY_TOKEN',
88
+ scope: workspaceStoreScope,
89
+ });
85
90
 
86
91
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
87
92
  expect(result).toBeUndefined();
@@ -98,7 +103,7 @@ describe('store transport', () => {
98
103
  const result = await listVariables({workspaceId: SECRETS_TEST_WORKSPACE_ID});
99
104
 
100
105
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
101
- expect(result).toEqual([variableListItem()]);
106
+ expect(result).toMatchObject([{key: 'LOG_LEVEL', value: 'debug', valueState: 'preview'}]);
102
107
  expect(request.url).toBe(
103
108
  `https://api.example.test/workspaces/${SECRETS_TEST_WORKSPACE_ID}/variables?limit=${SECRETS_MAX_LIST_LIMIT}`,
104
109
  );
@@ -113,7 +118,7 @@ describe('store transport', () => {
113
118
  const result = await getVariable({workspaceId: SECRETS_TEST_WORKSPACE_ID, key: 'LOG_LEVEL'});
114
119
 
115
120
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
116
- expect(result.value).toBe('first line\nsecond line');
121
+ expect(result).toMatchObject({value: 'first line\nsecond line', valueState: 'full'});
117
122
  expect(request.method).toBe('GET');
118
123
  expect(request.url).toBe(
119
124
  `https://api.example.test/workspaces/${SECRETS_TEST_WORKSPACE_ID}/variables/LOG_LEVEL`,
@@ -132,11 +137,12 @@ describe('store transport', () => {
132
137
  const result = await putVariable({
133
138
  workspaceId: SECRETS_TEST_WORKSPACE_ID,
134
139
  key: 'MY_KEY',
135
- body: {value: 'v'},
140
+ value: 'v',
141
+ scope: workspaceStoreScope,
136
142
  });
137
143
 
138
144
  expect(result.warnings).toEqual([{code: 'sensitive-variable-name', key: 'MY_KEY'}]);
139
- expect(result.item.value).toBe('v');
145
+ expect(result.item).toMatchObject({value: 'v', valueState: 'full'});
140
146
  });
141
147
 
142
148
  test('deletes a variable within a project scope via project_id query', async () => {
@@ -146,7 +152,7 @@ describe('store transport', () => {
146
152
  await deleteVariable({
147
153
  workspaceId: SECRETS_TEST_WORKSPACE_ID,
148
154
  key: 'LOG_LEVEL',
149
- projectId: '33333333-3333-4333-8333-333333333333',
155
+ scope: {kind: 'project', projectId: '33333333-3333-4333-8333-333333333333'},
150
156
  });
151
157
 
152
158
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
@@ -1,26 +1,23 @@
1
- import type {
2
- ListSecretsResponseDto,
3
- PutSecretBodyDto,
4
- PutSecretResponseDto,
5
- SecretDto,
6
- } from '@shipfox/api-secrets-dto';
1
+ import {listSecretsResponseSchema, putSecretResponseSchema} from '@shipfox/api-secrets-dto';
7
2
  import {createStoreApi} from './create-store-api.js';
3
+ import {toSecretMetadata, toStoreWriteWarnings} from './store-mapper.js';
8
4
 
9
- const secretsApi = createStoreApi<
10
- SecretDto,
11
- ListSecretsResponseDto,
12
- PutSecretBodyDto,
13
- PutSecretResponseDto
14
- >({
5
+ const secretsApi = createStoreApi({
15
6
  resource: 'secrets',
16
- listItems: (response) => response.secrets,
17
- putItem: (response) => response.secret,
7
+ listResponseSchema: listSecretsResponseSchema,
8
+ putResponseSchema: putSecretResponseSchema,
9
+ toListItems: (response) => response.secrets.map(toSecretMetadata),
10
+ toPutResult: (response) => ({
11
+ item: toSecretMetadata(response.secret),
12
+ warnings: toStoreWriteWarnings(response.warnings),
13
+ }),
18
14
  });
19
15
 
20
16
  export const secretsQueryKeys = secretsApi.queryKeys;
21
17
  export const listSecrets = secretsApi.listAll;
22
18
  export const putSecret = secretsApi.put;
23
19
  export const deleteSecret = secretsApi.remove;
20
+ export const listSecretsQueryOptions = secretsApi.listQueryOptions;
24
21
  export const useSecretsQuery = secretsApi.useListQuery;
25
22
  export const usePutSecretMutation = secretsApi.usePutMutation;
26
23
  export const useDeleteSecretMutation = secretsApi.useDeleteMutation;
@@ -0,0 +1,39 @@
1
+ import {secret, variable, variableListItem} from '#test/fixtures/secrets.js';
2
+ import {
3
+ toSecretMetadata,
4
+ toStoreWriteWarnings,
5
+ toVariable,
6
+ toVariablePreview,
7
+ } from './store-mapper.js';
8
+
9
+ describe('store transport mapping', () => {
10
+ test('maps secret metadata and workspace scope into a package-owned model', () => {
11
+ expect(toSecretMetadata(secret())).toEqual({
12
+ key: 'MY_TOKEN',
13
+ scope: {kind: 'workspace'},
14
+ createdAt: '2026-05-08T00:00:00.000Z',
15
+ updatedAt: '2026-05-08T00:00:00.000Z',
16
+ lastEditedBy: '22222222-2222-4222-8222-222222222222',
17
+ });
18
+ });
19
+
20
+ test('keeps truncated list values distinct from full detail values', () => {
21
+ expect(
22
+ toVariablePreview(variableListItem({value: 'preview', value_truncated: true})),
23
+ ).toMatchObject({
24
+ value: 'preview',
25
+ valueState: 'preview',
26
+ valueTruncated: true,
27
+ });
28
+ expect(toVariable(variable({value: 'complete value'}))).toMatchObject({
29
+ value: 'complete value',
30
+ valueState: 'full',
31
+ });
32
+ });
33
+
34
+ test('maps server warning DTOs to the store warning model', () => {
35
+ expect(toStoreWriteWarnings([{code: 'short-secret-value', key: 'MY_TOKEN'}])).toEqual([
36
+ {code: 'short-secret-value', key: 'MY_TOKEN'},
37
+ ]);
38
+ });
39
+ });
@@ -0,0 +1,47 @@
1
+ import type {
2
+ SecretDto,
3
+ SecretWriteWarningDto,
4
+ VariableDto,
5
+ VariableListItemDto,
6
+ } from '@shipfox/api-secrets-dto';
7
+ import type {
8
+ SecretMetadata,
9
+ StoreMetadata,
10
+ StoreWriteWarning,
11
+ Variable,
12
+ VariablePreview,
13
+ } from '#core/store.js';
14
+ import {scopeFromProjectId} from '#core/store.js';
15
+
16
+ function toStoreMetadata(item: SecretDto): StoreMetadata {
17
+ return {
18
+ key: item.key,
19
+ scope: scopeFromProjectId(item.project_id),
20
+ createdAt: item.created_at,
21
+ updatedAt: item.updated_at,
22
+ lastEditedBy: item.last_edited_by,
23
+ };
24
+ }
25
+
26
+ export function toSecretMetadata(secret: SecretDto): SecretMetadata {
27
+ return toStoreMetadata(secret);
28
+ }
29
+
30
+ export function toVariablePreview(variable: VariableListItemDto): VariablePreview {
31
+ return {
32
+ ...toStoreMetadata(variable),
33
+ value: variable.value,
34
+ valueState: 'preview',
35
+ valueTruncated: variable.value_truncated,
36
+ };
37
+ }
38
+
39
+ export function toVariable(variable: VariableDto): Variable {
40
+ return {...toStoreMetadata(variable), value: variable.value, valueState: 'full'};
41
+ }
42
+
43
+ export function toStoreWriteWarnings(
44
+ warnings: readonly SecretWriteWarningDto[],
45
+ ): StoreWriteWarning[] {
46
+ return warnings.map((warning) => ({code: warning.code, key: warning.key}));
47
+ }
@@ -1,61 +1,77 @@
1
- import type {
2
- GetVariableResponseDto,
3
- ListVariablesResponseDto,
4
- PutVariableBodyDto,
5
- PutVariableResponseDto,
6
- VariableDto,
7
- VariableListItemDto,
1
+ import {
2
+ getVariableResponseSchema,
3
+ listVariablesResponseSchema,
4
+ putVariableResponseSchema,
8
5
  } from '@shipfox/api-secrets-dto';
9
- import {apiRequest} from '@shipfox/client-api';
10
- import {useQuery} from '@tanstack/react-query';
6
+ import {checkedApiRequest} from '@shipfox/client-api';
7
+ import {type FetchQueryOptions, queryOptions, useQuery} from '@tanstack/react-query';
8
+ import {
9
+ projectIdFromScope,
10
+ type StoreScope,
11
+ type Variable,
12
+ workspaceStoreScope,
13
+ } from '#core/store.js';
11
14
  import {createStoreApi} from './create-store-api.js';
15
+ import {toStoreWriteWarnings, toVariable, toVariablePreview} from './store-mapper.js';
12
16
 
13
- const variablesApi = createStoreApi<
14
- VariableListItemDto,
15
- ListVariablesResponseDto,
16
- PutVariableBodyDto,
17
- PutVariableResponseDto
18
- >({
17
+ const variablesApi = createStoreApi({
19
18
  resource: 'variables',
20
- listItems: (response) => response.variables,
21
- // The list carries only a preview; a freshly-written value is the full value.
22
- putItem: (response) => ({...response.variable, value_truncated: false}),
19
+ listResponseSchema: listVariablesResponseSchema,
20
+ putResponseSchema: putVariableResponseSchema,
21
+ toListItems: (response) => response.variables.map(toVariablePreview),
22
+ toPutResult: (response) => ({
23
+ item: toVariable(response.variable),
24
+ warnings: toStoreWriteWarnings(response.warnings),
25
+ }),
23
26
  });
24
27
 
25
28
  export const variablesQueryKeys = variablesApi.queryKeys;
26
29
  export const listVariables = variablesApi.listAll;
27
30
  export const putVariable = variablesApi.put;
28
31
  export const deleteVariable = variablesApi.remove;
32
+ export const listVariablesQueryOptions = variablesApi.listQueryOptions;
29
33
  export const useVariablesQuery = variablesApi.useListQuery;
30
34
  export const usePutVariableMutation = variablesApi.usePutMutation;
31
35
  export const useDeleteVariableMutation = variablesApi.useDeleteMutation;
32
36
 
33
- /**
34
- * Reads a single variable including its full value. The list only returns a
35
- * bounded preview, so editing a truncated variable fetches the full value here.
36
- */
37
+ export const variableQueryKey = (workspaceId: string, key: string, scope: StoreScope) =>
38
+ [...variablesQueryKeys.all, 'detail', workspaceId, key, scope] as const;
39
+
40
+ type VariableQueryOptions = FetchQueryOptions<
41
+ Variable,
42
+ Error,
43
+ Variable,
44
+ ReturnType<typeof variableQueryKey>
45
+ >;
46
+
37
47
  export async function getVariable(params: {
38
48
  workspaceId: string;
39
49
  key: string;
40
- projectId?: string | undefined;
50
+ scope?: StoreScope;
41
51
  signal?: AbortSignal | undefined;
42
- }): Promise<VariableDto> {
43
- const search = params.projectId
44
- ? `?${new URLSearchParams({project_id: params.projectId}).toString()}`
45
- : '';
46
- const response = await apiRequest<GetVariableResponseDto>(
52
+ }) {
53
+ const scope = params.scope ?? workspaceStoreScope;
54
+ const projectId = projectIdFromScope(scope);
55
+ const search = projectId ? `?${new URLSearchParams({project_id: projectId}).toString()}` : '';
56
+ const response = await checkedApiRequest(
57
+ getVariableResponseSchema,
47
58
  `/workspaces/${params.workspaceId}/variables/${encodeURIComponent(params.key)}${search}`,
48
59
  {signal: params.signal},
49
60
  );
50
- return response.variable;
61
+ return toVariable(response.variable);
51
62
  }
52
63
 
53
- export function useVariableQuery(workspaceId: string, key: string | undefined) {
54
- return useQuery({
55
- queryKey: key
56
- ? [...variablesQueryKeys.all, 'detail', workspaceId, key]
57
- : [...variablesQueryKeys.all, 'detail'],
58
- enabled: Boolean(key),
59
- queryFn: ({signal}) => getVariable({workspaceId, key: key ?? '', signal}),
64
+ export function variableQueryOptions(
65
+ workspaceId: string,
66
+ key: string,
67
+ scope: StoreScope = workspaceStoreScope,
68
+ ): VariableQueryOptions {
69
+ return queryOptions({
70
+ queryKey: variableQueryKey(workspaceId, key, scope),
71
+ queryFn: ({signal}) => getVariable({workspaceId, key, scope, signal}),
60
72
  });
61
73
  }
74
+
75
+ export function useVariableQuery(workspaceId: string, key: string | undefined) {
76
+ return useQuery({...variableQueryOptions(workspaceId, key ?? ''), enabled: Boolean(key)});
77
+ }