@checkstack/frontend-api 0.3.8 → 0.3.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # @checkstack/frontend-api
2
2
 
3
+ ## 0.3.10
4
+
5
+ ### Patch Changes
6
+
7
+ - c4e7560: Fix data integrity, cache invalidation, and mobile UI issues
8
+
9
+ - **Centralized mutation cache invalidation**: Every mutation now automatically invalidates its plugin's query cache on success via the shared `createProcedureHook` in `orpc-query.tsx`. This ensures all views stay in sync without requiring individual components to remember manual `invalidateQueries` calls.
10
+ - **Fixed oRPC query key matching**: Query keys use nested arrays (`[["pluginId"]]`) to correctly match oRPC's `[pathArray, options]` key structure. Fixed the broken flat-string pattern in `SystemBadgeDataProvider`.
11
+ - **Fixed hourly aggregation duplication**: Added `NULLS NOT DISTINCT` to the `health_check_aggregates` unique constraint so local runs (`source_id = NULL`) correctly conflict-match instead of creating duplicate hourly buckets. Includes a migration to clean up existing duplicates.
12
+ - **Fixed modal scrolling on mobile**: Added `max-height` + `overflow-y-auto` to `ConfirmationModal`, and refactored `Dialog` from translate-centering to flex-centering with `dvh` units for reliable mobile scroll containment.
13
+
14
+ ## 0.3.9
15
+
16
+ ### Patch Changes
17
+
18
+ - d1a2796: Enforce stricter code quality standards and eliminate AI slop anti-patterns.
19
+
20
+ **New utility**
21
+
22
+ - `extractErrorMessage(error, fallback?)` in `@checkstack/common` for consistent error extraction
23
+
24
+ **ESLint rules**
25
+
26
+ - `react-hooks/rules-of-hooks` and `exhaustive-deps` for hook correctness
27
+ - `no-console` in frontend packages — forces `toast` over silent `console.error`
28
+ - `no-restricted-syntax` banning `instanceof Error` — forces `extractErrorMessage`
29
+ - Custom `no-eslint-disable-any` rule preventing `@typescript-eslint/no-explicit-any` circumvention
30
+
31
+ **Refactoring**
32
+
33
+ - Replace 141 `instanceof Error` boilerplate patterns across the codebase
34
+ - Replace swallowed `console.error` with user-visible `toast.error()` feedback
35
+ - Remove 15 redundant `as` type casts in IntegrationsPage and ProviderConnectionsPage
36
+ - Consolidate 3 identical callback handlers into `handleDialogClose`
37
+ - Fix conditional React hook call in `FormField.tsx`
38
+ - Fix unstable useMemo deps in `Dashboard.tsx`
39
+ - Replace `useEffect`→`setState` with derived `useMemo` in `RegisterPage.tsx`
40
+ - Rewrite `keystore.test.ts` with typed `DrizzleMockChain` (eliminating 7 `any` suppressions)
41
+ - Delete obvious comments in `encryption.ts` and Teams `provider.ts`
42
+
43
+ - Updated dependencies [d1a2796]
44
+ - @checkstack/common@0.6.5
45
+
3
46
  ## 0.3.8
4
47
 
5
48
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/frontend-api",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
@@ -12,7 +12,7 @@
12
12
  "react": "^18.0.0"
13
13
  },
14
14
  "dependencies": {
15
- "@checkstack/common": "0.6.3",
15
+ "@checkstack/common": "0.6.5",
16
16
  "@orpc/client": "^1.13.14",
17
17
  "@orpc/react-query": "1.13.4",
18
18
  "@orpc/tanstack-query": "^1.13.2",
@@ -22,8 +22,8 @@
22
22
  "@types/bun": "^1.3.5",
23
23
  "@types/react": "^18.0.0",
24
24
  "typescript": "^5.0.0",
25
- "@checkstack/tsconfig": "0.0.3",
26
- "@checkstack/scripts": "0.1.1"
25
+ "@checkstack/tsconfig": "0.0.5",
26
+ "@checkstack/scripts": "0.1.2"
27
27
  },
28
28
  "checkstack": {
29
29
  "type": "tooling"
@@ -37,8 +37,7 @@ export function ExtensionSlot<TSlot extends SlotDefinition<unknown>>({
37
37
  return (
38
38
  <>
39
39
  {extensions.map((ext) => {
40
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
- const Component = ext.component as React.ComponentType<any>;
40
+ const Component = ext.component as React.ComponentType<Record<string, unknown>>;
42
41
  return <Component key={ext.id} {...(context ?? {})} />;
43
42
  })}
44
43
  </>
@@ -14,9 +14,8 @@ import {
14
14
  type UseMutationOptions,
15
15
  } from "@tanstack/react-query";
16
16
 
17
- // Re-export useQueryClient for cache invalidation in consuming packages
18
- // This ensures all packages use the same React Query instance
19
17
  export { useQueryClient } from "@tanstack/react-query";
18
+ import { useQueryClient } from "@tanstack/react-query";
20
19
  import { useApi } from "./api-context";
21
20
  import { rpcApiRef } from "./core-apis";
22
21
  import type { ClientDefinition, InferClient } from "@checkstack/common";
@@ -71,7 +70,7 @@ function useOrpcUtils(): OrpcUtils {
71
70
  if (!context) {
72
71
  throw new Error(
73
72
  "usePluginClient must be used within OrpcQueryProvider. " +
74
- "Wrap your app with <OrpcQueryProvider>."
73
+ "Wrap your app with <OrpcQueryProvider>.",
75
74
  );
76
75
  }
77
76
  return context;
@@ -88,7 +87,7 @@ function useOrpcUtils(): OrpcUtils {
88
87
  interface QueryProcedure<TInput, TOutput> {
89
88
  useQuery: (
90
89
  input?: TInput,
91
- options?: Omit<UseQueryOptions<TOutput, Error>, "queryKey" | "queryFn">
90
+ options?: Omit<UseQueryOptions<TOutput, Error>, "queryKey" | "queryFn">,
92
91
  ) => UseQueryResult<TOutput, Error>;
93
92
  }
94
93
 
@@ -101,7 +100,7 @@ interface MutationProcedure<TInput, TOutput> {
101
100
  options?: Omit<
102
101
  UseMutationOptions<TOutput, Error, TInput>,
103
102
  "mutationFn" | "mutationKey"
104
- >
103
+ >,
105
104
  ) => UseMutationResult<TOutput, Error, TInput>;
106
105
  }
107
106
 
@@ -148,14 +147,15 @@ type InferSchemaOutputType<T> = T extends { _output: infer O } ? O : unknown;
148
147
  /**
149
148
  * Check if a contract type is a procedure (not a nested router).
150
149
  */
151
- type IsContractProcedure<T> = T extends ContractProcedure<
152
- infer _TInput,
153
- infer _TOutput,
154
- infer _TErrors,
155
- infer _TMeta
156
- >
157
- ? true
158
- : false;
150
+ type IsContractProcedure<T> =
151
+ T extends ContractProcedure<
152
+ infer _TInput,
153
+ infer _TOutput,
154
+ infer _TErrors,
155
+ infer _TMeta
156
+ >
157
+ ? true
158
+ : false;
159
159
 
160
160
  /**
161
161
  * Maps a contract router to wrapped procedures based on operationType.
@@ -167,10 +167,10 @@ type WrappedClient<TContract extends AnyContractRouter, TClient> = {
167
167
  > extends true
168
168
  ? WrappedProcedure<TContract[K], TClient[K]>
169
169
  : TClient[K] extends object
170
- ? TContract[K] extends AnyContractRouter
171
- ? WrappedClient<TContract[K], TClient[K]>
172
- : never
173
- : never;
170
+ ? TContract[K] extends AnyContractRouter
171
+ ? WrappedClient<TContract[K], TClient[K]>
172
+ : never
173
+ : never;
174
174
  };
175
175
 
176
176
  // =============================================================================
@@ -202,7 +202,7 @@ type WrappedClient<TContract extends AnyContractRouter, TClient> = {
202
202
  * ```
203
203
  */
204
204
  export function usePluginClient<T extends ClientDefinition>(
205
- definition: T
205
+ definition: T,
206
206
  ): WrappedClient<NonNullable<T["__contractType"]>, InferClient<T>> {
207
207
  const orpcUtils = useOrpcUtils();
208
208
 
@@ -213,7 +213,7 @@ export function usePluginClient<T extends ClientDefinition>(
213
213
  if (!pluginUtils) {
214
214
  throw new Error(
215
215
  `Plugin "${definition.pluginId}" not found. ` +
216
- `Ensure the plugin is registered and the backend is running.`
216
+ `Ensure the plugin is registered and the backend is running.`,
217
217
  );
218
218
  }
219
219
 
@@ -221,8 +221,8 @@ export function usePluginClient<T extends ClientDefinition>(
221
221
  const contract = definition.contract as Record<string, unknown>;
222
222
 
223
223
  return useMemo(() => {
224
- return wrapPluginUtils(pluginUtils, contract);
225
- }, [pluginUtils, contract]) as WrappedClient<
224
+ return wrapPluginUtils(pluginUtils, contract, definition.pluginId);
225
+ }, [pluginUtils, contract, definition.pluginId]) as WrappedClient<
226
226
  NonNullable<T["__contractType"]>,
227
227
  InferClient<T>
228
228
  >;
@@ -234,7 +234,8 @@ export function usePluginClient<T extends ClientDefinition>(
234
234
 
235
235
  function wrapPluginUtils(
236
236
  utils: Record<string, unknown>,
237
- contract: Record<string, unknown>
237
+ contract: Record<string, unknown>,
238
+ pluginId: string,
238
239
  ): Record<string, unknown> {
239
240
  const wrapped: Record<string, unknown> = {};
240
241
 
@@ -276,13 +277,15 @@ function wrapPluginUtils(
276
277
  unknown,
277
278
  Error
278
279
  >,
279
- operationType
280
+ operationType,
281
+ pluginId,
280
282
  );
281
283
  } else {
282
284
  // Nested namespace - recurse
283
285
  wrapped[key] = wrapPluginUtils(
284
286
  procedureUtils as Record<string, unknown>,
285
- (contractProcedure || {}) as Record<string, unknown>
287
+ (contractProcedure || {}) as Record<string, unknown>,
288
+ pluginId,
286
289
  );
287
290
  }
288
291
  }
@@ -296,7 +299,7 @@ function wrapPluginUtils(
296
299
  */
297
300
  function getOperationType(
298
301
  contractProcedure: Record<string, unknown> | undefined,
299
- procedureName?: string
302
+ procedureName?: string,
300
303
  ): "query" | "mutation" {
301
304
  const orpcMeta = contractProcedure?.["~orpc"] as
302
305
  | { meta?: { operationType?: "query" | "mutation" } }
@@ -309,7 +312,7 @@ function getOperationType(
309
312
  `Procedure ${
310
313
  procedureName ? `"${procedureName}" ` : ""
311
314
  }is missing required "operationType" in contract metadata. ` +
312
- `Add operationType: "query" or operationType: "mutation" to the procedure's .meta() call.`
315
+ `Add operationType: "query" or operationType: "mutation" to the procedure's .meta() call.`,
313
316
  );
314
317
  }
315
318
 
@@ -321,13 +324,31 @@ function getOperationType(
321
324
  */
322
325
  function createProcedureHook<TInput, TOutput>(
323
326
  proc: ProcedureUtils<ClientContext, TInput, TOutput, Error>,
324
- operationType: "query" | "mutation"
327
+ operationType: "query" | "mutation",
328
+ pluginId: string,
325
329
  ): QueryProcedure<TInput, TOutput> | MutationProcedure<TInput, TOutput> {
326
330
  if (operationType === "mutation") {
327
331
  return {
328
332
  useMutation: (options) => {
329
- const mutationOpts = proc.mutationOptions(options);
330
- return useMutation(mutationOpts);
333
+ const queryClient = useQueryClient();
334
+ const mutationOpts = proc.mutationOptions({
335
+ ...options,
336
+ onSuccess: (...args) => {
337
+ // Automatically invalidate all queries for this plugin
338
+ // so every view showing this plugin's data stays fresh.
339
+ // oRPC query keys are [pathArray, options] where pathArray
340
+ // starts with the pluginId, e.g. ["healthcheck", "getConfigurations"].
341
+ void queryClient.invalidateQueries({
342
+ queryKey: [[pluginId]],
343
+ });
344
+ // Call the user-provided onSuccess handler if present
345
+ options?.onSuccess?.(...args);
346
+ },
347
+ });
348
+ // Remove the duplicate onSuccess from the outer options to avoid
349
+ // double-invocation — it's already integrated above.
350
+ const { onSuccess: _, ...restOptions } = options ?? {};
351
+ return useMutation({ ...mutationOpts, ...restOptions });
331
352
  },
332
353
  };
333
354
  }
@@ -116,6 +116,7 @@ export const RuntimeConfigProvider: React.FC<RuntimeConfigProviderProps> = ({
116
116
  } catch (error_) {
117
117
  console.error("RuntimeConfigProvider: Failed to load config", error_);
118
118
  setError(
119
+ // eslint-disable-next-line no-restricted-syntax -- Needs Error object for state, not just message
119
120
  error_ instanceof Error ? error_ : new Error(String(error_))
120
121
  );
121
122
  // Do NOT set a fallback — surface the error visibly.