@checkstack/frontend-api 0.3.9 → 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,16 @@
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
+
3
14
  ## 0.3.9
4
15
 
5
16
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/frontend-api",
3
- "version": "0.3.9",
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.4",
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",
@@ -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
  }