Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions packages/stack/registry/btst-comments.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/stack/scripts/build-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ const PLUGINS: PluginConfig[] = [
"Customize the UI layer while keeping data-fetching in @btst/stack.",
extraNpmDeps: ["date-fns"],
extraRegistryDeps: [],
pluginRootFiles: ["types.ts", "schemas.ts", "error-utils.ts"],
pluginRootFiles: ["types.ts", "schemas.ts"],
},
{
name: "ui-builder",
Expand Down
108 changes: 108 additions & 0 deletions packages/stack/src/__tests__/comments-query-keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* SSG guard: the factory-generated comments query keys must stay deep-equal
* to the `COMMENTS_QUERY_KEYS` builders (and their shared discriminators)
* used by loader/SSG prefetch paths. Key drift breaks React Query cache
* hydration silently.
*/
import { describe, expect, it, vi } from "vitest";
import {
COMMENTS_QUERY_KEYS,
commentCountDiscriminator,
} from "../plugins/comments/api/query-key-defs";
import { createCommentsQueryKeys } from "../plugins/comments/query-keys";

const client = vi.fn() as any;

describe("comments query keys match SSG prefetch keys", () => {
const queries = createCommentsQueryKeys(client);

it("comments list keys match for default params", () => {
expect([...queries.comments.list({}).queryKey]).toEqual([
...COMMENTS_QUERY_KEYS.commentsList({}),
]);
});

it("comments list keys match the moderation loader prefetch key", () => {
const params = { status: "pending" as const, limit: 20, offset: 0 };
expect([...queries.comments.list(params).queryKey]).toEqual([
...COMMENTS_QUERY_KEYS.commentsList(params),
]);
});

it("comments list keys match the user-comments loader prefetch key", () => {
const params = {
authorId: "user-1",
sort: "desc" as const,
limit: 20,
offset: 0,
};
expect([...queries.comments.list(params).queryKey]).toEqual([
...COMMENTS_QUERY_KEYS.commentsList(params),
]);
});

it("distinguishes parentId null from undefined (separate cache entries)", () => {
const withNull = queries.comments.list({ parentId: null }).queryKey;
const withUndefined = queries.comments.list({}).queryKey;
expect([...withNull]).toEqual([
...COMMENTS_QUERY_KEYS.commentsList({ parentId: null }),
]);
expect(withNull).not.toEqual(withUndefined);
});

it("segregates caches per currentUserId without leaking it to the key builders", () => {
const params = {
resourceId: "post-1",
resourceType: "post",
currentUserId: "user-9",
};
expect([...queries.comments.list(params).queryKey]).toEqual([
...COMMENTS_QUERY_KEYS.commentsList(params),
]);
});

it("thread keys match and exclude offset (pageParam-driven)", () => {
const params = {
resourceId: "post-1",
resourceType: "post",
parentId: null,
status: "approved" as const,
sort: "asc" as const,
limit: 10,
};
expect([...queries.commentsThread.list(params).queryKey]).toEqual([
...COMMENTS_QUERY_KEYS.commentsThread(params),
]);
});

it("count keys use the shared discriminator", () => {
// Note: COMMENTS_QUERY_KEYS.commentCount uses the ["comments", "count"]
// prefix while the runtime factory has always used
// ["commentCount", "byResource"] — a pre-existing divergence. The
// discriminator cell (the part that actually varies) must stay shared.
const params = { resourceId: "post-1", resourceType: "post" };
expect([...queries.commentCount.byResource(params).queryKey]).toEqual([
"commentCount",
"byResource",
commentCountDiscriminator(params),
]);
expect(COMMENTS_QUERY_KEYS.commentCount(params)[2]).toEqual(
commentCountDiscriminator(params),
);
});

it("exposes the same _def prefixes as the previous factory", () => {
expect([...queries.comments._def]).toEqual(["comments"]);
expect([...queries.comments.list._def]).toEqual(["comments", "list"]);
expect([...queries.commentCount._def]).toEqual(["commentCount"]);
expect([...queries.commentCount.byResource._def]).toEqual([
"commentCount",
"byResource",
]);
expect([...queries.commentsThread._def]).toEqual(["commentsThread"]);
expect([...queries.commentsThread.list._def]).toEqual([
"commentsThread",
"list",
]);
});
});
7 changes: 7 additions & 0 deletions packages/stack/src/plugins/client/resource/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,17 @@ export async function runResourceQuery(

/**
* Executes a mutation declaration: fetch → error-check → unwrap.
*
* `headers` supports plugins whose public hooks take an explicit client
* config (e.g. embeddable components) instead of resolving it from
* `usePluginOverrides` — same as the `headers` parameter on
* `createResourceQueryKeys`.
*/
export async function runResourceMutation(
client: ResourceClient,
def: ResourceMutationDef<any, any>,
vars: unknown,
headers?: HeadersInit,
): Promise<unknown> {
const { body, params, query } = def.input
? def.input(vars)
Expand All @@ -252,6 +258,7 @@ export async function runResourceMutation(
...(body !== undefined ? { body } : {}),
...(params !== undefined ? { params } : {}),
...(query !== undefined ? { query } : {}),
...(headers !== undefined ? { headers } : {}),
});

if (isErrorResponse(response)) {
Expand Down
Loading
Loading