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
5 changes: 5 additions & 0 deletions .changeset/slimy-cows-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"swr-openapi": minor
---

Disallow extra properties in swr-openapi init types
207 changes: 184 additions & 23 deletions packages/swr-openapi/src/__test__/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,44 @@ describe("types", () => {
useQuery("/pet/findByStatus", null);
});
});

describe("rejects extra properties", () => {
it("in query params", () => {
useQuery("/pet/findByStatus", {
params: {
query: {
status: "available",
// @ts-expect-error extra property should be rejected
invalid_property: "nope",
},
},
});
});

it("in path params", () => {
useQuery("/pet/{petId}", {
params: {
path: {
petId: 5,
// @ts-expect-error extra property should be rejected
invalid_path_param: "nope",
},
},
});
});

it("in header params", () => {
useQuery("/pet/findByStatus", {
params: {
header: {
"X-Example": "test",
// @ts-expect-error extra property should be rejected
"Invalid-Header": "nope",
},
},
});
});
});
});

describe("useImmutable", () => {
Expand Down Expand Up @@ -122,6 +160,44 @@ describe("types", () => {
useImmutable("/pet/findByStatus", null);
});
});

describe("rejects extra properties", () => {
it("in query params", () => {
useImmutable("/pet/findByStatus", {
params: {
query: {
status: "available",
// @ts-expect-error extra property should be rejected
invalid_property: "nope",
},
},
});
});

it("in path params", () => {
useImmutable("/pet/{petId}", {
params: {
path: {
petId: 5,
// @ts-expect-error extra property should be rejected
invalid_path_param: "nope",
},
},
});
});

it("in header params", () => {
useImmutable("/pet/findByStatus", {
params: {
header: {
"X-Example": "test",
// @ts-expect-error extra property should be rejected
"Invalid-Header": "nope",
},
},
});
});
});
});

describe("useInfinite", () => {
Expand Down Expand Up @@ -154,40 +230,47 @@ describe("types", () => {
useInfinite("/pet/findByStatus", () => null);
});
});
});

describe("useMutate -> mutate", () => {
it("accepts path alone", async () => {
await mutate(["/pet/{petId}"]);
});
describe("rejects extra properties", () => {
it("in query params", () => {
useInfinite("/pet/findByStatus", () => ({
params: {
query: {
status: "available",
// @ts-expect-error extra property should be rejected
invalid_property: "nope",
},
},
}));
});

it("accepts path and init", async () => {
await mutate([
"/pet/{petId}",
{
it("in path params", () => {
useInfinite("/pet/{petId}", () => ({
params: {
path: {
petId: 5,
// @ts-expect-error extra property should be rejected
invalid_path_param: "nope",
},
},
},
]);
});

it("accepts partial init", async () => {
await mutate(["/pet/{petId}", { params: {} }]);
});
}));
});

it("does not accept `null` init", async () => {
await mutate([
"/pet/{petId}",
// @ts-expect-error null not accepted
null,
]);
it("in header params", () => {
useInfinite("/pet/findByStatus", () => ({
params: {
header: {
"X-Example": "test",
// @ts-expect-error extra property should be rejected
"Invalid-Header": "nope",
},
},
}));
});
});
});

describe("when init is not required", () => {
describe("useMutate -> mutate", () => {
it("accepts path alone", async () => {
await mutate(["/pet/{petId}"]);
});
Expand Down Expand Up @@ -216,6 +299,84 @@ describe("types", () => {
null,
]);
});

describe("when init is not required", () => {
it("accepts path alone", async () => {
await mutate(["/pet/{petId}"]);
});

it("accepts path and init", async () => {
await mutate([
"/pet/{petId}",
{
params: {
path: {
petId: 5,
},
},
},
]);
});

it("accepts partial init", async () => {
await mutate(["/pet/{petId}", { params: {} }]);
});

it("does not accept `null` init", async () => {
await mutate([
"/pet/{petId}",
// @ts-expect-error null not accepted
null,
]);
});
});

describe("rejects extra properties", () => {
it("in path", () => {
mutate([
"/pet/{petId}",
{
params: {
path: {
petId: 5,
// @ts-expect-error extra property should be rejected
invalid_path_param: "no",
},
},
},
]);
});

it("in query params", () => {
mutate([
"/pet/findByStatus",
{
params: {
query: {
status: "available",
// @ts-expect-error extra property should be rejected
invalid_property: "nope",
},
},
},
]);
});

it("in header params", () => {
mutate([
"/pet/findByStatus",
{
params: {
header: {
"X-Example": "test",
// @ts-expect-error extra property should be rejected
"Invalid-Header": "nope",
},
},
},
]);
});
});
});
});

Expand Down
3 changes: 2 additions & 1 deletion packages/swr-openapi/src/infinite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useSWRInfinite, {
} from "swr/infinite";
import type { TypesForGetRequest } from "./types.js";
import { useCallback, useDebugValue } from "react";
import type { Exact } from "type-fest";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to know about this lib!


/**
* Produces a typed wrapper for [`useSWRInfinite`](https://swr.vercel.app/docs/pagination#useswrinfinite).
Expand Down Expand Up @@ -42,7 +43,7 @@ export function createInfiniteHook<
return function useInfinite<
Path extends PathsWithMethod<Paths, "get">,
R extends TypesForGetRequest<Paths, Path>,
Init extends R["Init"],
Init extends Exact<R["Init"], Init>,
Data extends R["Data"],
Error extends R["Error"] | FetcherError,
Config extends SWRInfiniteConfiguration<Data, Error>,
Expand Down
4 changes: 2 additions & 2 deletions packages/swr-openapi/src/mutate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Client } from "openapi-fetch";
import type { MediaType, PathsWithMethod } from "openapi-typescript-helpers";
import { useCallback, useDebugValue } from "react";
import { type MutatorCallback, type MutatorOptions, useSWRConfig } from "swr";
import type { PartialDeep } from "type-fest";
import type { Exact, PartialDeep } from "type-fest";
import type { TypesForGetRequest } from "./types.js";

// Types are loose here to support ecosystem utilities like `_.isMatch`
Expand Down Expand Up @@ -48,7 +48,7 @@ export function createMutateHook<Paths extends {}, IMediaType extends MediaType>
function mutate<
Path extends PathsWithMethod<Paths, "get">,
R extends TypesForGetRequest<Paths, Path>,
Init extends R["Init"],
Init extends Exact<R["Init"], Init>,
>(
[path, init]: [Path, PartialDeep<Init>?],
data?: R["Data"] | Promise<R["Data"]> | MutatorCallback<R["Data"]>,
Expand Down
3 changes: 2 additions & 1 deletion packages/swr-openapi/src/query-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescr
import type { Fetcher, SWRHook } from "swr";
import type { TypesForGetRequest } from "./types.js";
import { useCallback, useDebugValue, useMemo } from "react";
import type { Exact } from "type-fest";

/**
* @private
Expand All @@ -17,7 +18,7 @@ export function configureBaseQueryHook(useHook: SWRHook) {
return function useQuery<
Path extends PathsWithMethod<Paths, "get">,
R extends TypesForGetRequest<Paths, Path>,
Init extends R["Init"],
Init extends Exact<R["Init"], Init>,
Data extends R["Data"],
Error extends R["Error"] | FetcherError,
Config extends R["SWRConfig"],
Expand Down
Loading