Skip to content

Commit

Permalink
Merge pull request #372 from saiichihashimoto/client-types
Browse files Browse the repository at this point in the history
feat(client): a ton more typed methods #286
  • Loading branch information
kodiakhq[bot] authored Sep 27, 2023
2 parents 678ee74 + 7dd938b commit 4e3e8a9
Show file tree
Hide file tree
Showing 4 changed files with 731 additions and 37 deletions.
113 changes: 104 additions & 9 deletions packages/client/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ClientPerspective,
RequestFetchOptions,
} from "@sanity/client";
import type { SetOptional } from "type-fest";

import { expectType } from "@sanity-typed/test-utils";
import type { SanityDocument } from "@sanity-typed/types";
Expand Down Expand Up @@ -33,7 +34,7 @@ describe("createClient", () => {
return client.clone();
};

expectType<ReturnType<typeof execClone>>().toStrictEqual<
expectType<ReturnType<typeof execClone>>().toEqual<
ReturnType<typeof exec>
>();
});
Expand Down Expand Up @@ -101,7 +102,7 @@ describe("createClient", () => {
});
};

expectType<ReturnType<typeof execWithConfig>>().toStrictEqual<
expectType<ReturnType<typeof execWithConfig>>().toEqual<
ReturnType<typeof exec>
>();
});
Expand Down Expand Up @@ -133,7 +134,7 @@ describe("createClient", () => {
});
};

expectType<ReturnType<typeof execWithConfig>>().toStrictEqual<
expectType<ReturnType<typeof execWithConfig>>().toEqual<
ReturnType<typeof exec>
>();
});
Expand Down Expand Up @@ -310,21 +311,88 @@ describe("createClient", () => {
});

describe("create", () => {
it.todo("https://github.com/sanity-io/client#creating-documents");
it("requires a document without _ fields (optional _id) and returns the document", () => {
const exec = () => {
const client = createClient<{
bar: { _type: "bar"; bar: "bar" };
foo: AnySanityDocument & { _type: "foo" };
qux: AnySanityDocument & { _type: "qux" };
}>()({});

expectType<Parameters<typeof client.create>[0]>().toEqual<
Omit<
SetOptional<
| (AnySanityDocument & { _type: "foo" })
| (AnySanityDocument & { _type: "qux" }),
"_id"
>,
"_createdAt" | "_rev" | "_updatedAt"
>
>();

return client.create({ _type: "foo" });
};

expectType<ReturnType<typeof exec>>().toEqual<
Promise<AnySanityDocument & { _type: "foo" }>
>();
});

it.todo("https://github.com/sanity-io/client#mutation-options");
});

describe("createOrReplace", () => {
it.todo("https://github.com/sanity-io/client#creatingreplacing-documents");
it("requires a document without _ fields (required _id) and returns the document", () => {
const exec = () => {
const client = createClient<{
bar: { _type: "bar"; bar: "bar" };
foo: AnySanityDocument & { _type: "foo" };
qux: AnySanityDocument & { _type: "qux" };
}>()({});

expectType<Parameters<typeof client.createOrReplace>[0]>().toEqual<
Omit<
| (AnySanityDocument & { _type: "foo" })
| (AnySanityDocument & { _type: "qux" }),
"_createdAt" | "_rev" | "_updatedAt"
>
>();

return client.createOrReplace({ _type: "foo", _id: "id" });
};

expectType<ReturnType<typeof exec>>().toEqual<
Promise<AnySanityDocument & { _type: "foo" }>
>();
});

it.todo("https://github.com/sanity-io/client#mutation-options");
});

describe("createIfNotExists", () => {
it.todo(
"https://github.com/sanity-io/client#creating-if-not-already-present"
);
it("requires a document without _ fields (required _id) and returns the document", () => {
const exec = () => {
const client = createClient<{
bar: { _type: "bar"; bar: "bar" };
foo: AnySanityDocument & { _type: "foo" };
qux: AnySanityDocument & { _type: "qux" };
}>()({});

expectType<Parameters<typeof client.createIfNotExists>[0]>().toEqual<
Omit<
| (AnySanityDocument & { _type: "foo" })
| (AnySanityDocument & { _type: "qux" }),
"_createdAt" | "_rev" | "_updatedAt"
>
>();

return client.createIfNotExists({ _type: "foo", _id: "id" });
};

expectType<ReturnType<typeof exec>>().toEqual<
Promise<AnySanityDocument & { _type: "foo" }>
>();
});

it.todo("https://github.com/sanity-io/client#mutation-options");
});
Expand All @@ -334,7 +402,34 @@ describe("createClient", () => {
});

describe("delete", () => {
it.todo("https://github.com/sanity-io/client#delete-documents");
it("returns a union of the documents", () => {
const exec = () => {
const client = createClient<{
bar: { _type: "bar"; bar: "bar" };
foo: AnySanityDocument & { _type: "foo" };
qux: AnySanityDocument & { _type: "qux" };
}>()({});

return client.delete("id");
};

expectType<ReturnType<typeof exec>>().toEqual<
Promise<
| (AnySanityDocument & { _type: "foo" })
| (AnySanityDocument & { _type: "qux" })
>
>();
});

it.todo("handle MutationSelection that selects multiple documents via ids");

it.todo(
"handle MutationSelection that selects a single document via query"
);

it.todo(
"handle MutationSelection that selects multiple documents via query"
);

it.todo("https://github.com/sanity-io/client#deleting-an-asset");

Expand Down
173 changes: 157 additions & 16 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { createClient as createClientNative } from "@sanity/client";
import type {
BaseMutationOptions,
ClientConfig,
FilteredResponseQueryOptions,
InitializedClientConfig as InitializedClientConfigNative,
MultipleMutationResult,
MutationSelection,
ObservableSanityClient as ObservableSanityClientNative,
QueryParams,
RawQueryResponse as RawQueryResponseNative,
SanityClient as SanityClientNative,
SingleMutationResult,
UnfilteredResponseQueryOptions,
} from "@sanity/client";
import type { IsNever, Merge, WritableDeep } from "type-fest";
import type { Observable } from "rxjs";
import type { IsNever, Merge, SetOptional, WritableDeep } from "type-fest";

import type { ExecuteQuery, RootScope } from "@sanity-typed/groq";
import type { SanityDocument } from "@sanity-typed/types";

declare const README: unique symbol;

type AnySanityDocument = Omit<SanityDocument, "_type">;
type AnySanityDocument = Omit<SanityDocument, "_type"> & { _type: string };

export type InitializedClientConfig<TClientConfig extends ClientConfig> = Merge<
InitializedClientConfigNative,
Expand Down Expand Up @@ -46,22 +52,130 @@ type GetDocuments<Ids extends string[], TDocument extends AnySanityDocument> = {
| null;
};

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- recursive type
export interface SanityClient<
type PromiseOrObservable<
TIsPromise extends boolean,
T
> = TIsPromise extends true ? Promise<T> : Observable<T>;

type OverrideSanityClient<
TSanityClient,
TClientConfig extends ClientConfig,
TDocument extends AnySanityDocument
> extends Omit<
SanityClientNative,
"clone" | "config" | "fetch" | "getDocument" | "getDocuments" | "withConfig"
> {
clone: () => SanityClient<TClientConfig, TDocument>;
TDocument extends AnySanityDocument,
TIsPromise extends boolean
> = Omit<
TSanityClient,
| "clone"
| "config"
| "create"
| "createIfNotExists"
| "createOrReplace"
| "delete"
| "fetch"
| "getDocument"
| "getDocuments"
| "observable"
| "withConfig"
> & {
clone: () => OverrideSanityClient<
TSanityClient,
TClientConfig,
TDocument,
TIsPromise
>;
config: <
const NewConfig extends Partial<ClientConfig> | undefined = undefined
>(
newConfig?: NewConfig
) => NewConfig extends undefined
? InitializedClientConfig<WritableDeep<TClientConfig>>
: SanityClient<Merge<TClientConfig, NewConfig>, TDocument>;
: OverrideSanityClient<
TSanityClient,
Merge<TClientConfig, NewConfig>,
TDocument,
TIsPromise
>;
create: <
const Doc extends Omit<
SetOptional<TDocument, "_id">,
"_createdAt" | "_rev" | "_updatedAt"
> & { _type: string },
const Options extends BaseMutationOptions & {
returnDocuments?: boolean;
returnFirst?: boolean;
}
>(
document: Doc,
options?: Options
) => PromiseOrObservable<
TIsPromise,
Options extends { returnDocuments: false; returnFirst: false }
? MultipleMutationResult
: Options extends { returnFirst: false }
? Extract<TDocument, { _type: Doc["_type"] }>[]
: Options extends { returnDocuments: false }
? SingleMutationResult
: Extract<TDocument, { _type: Doc["_type"] }>
>;
createIfNotExists: <
const Doc extends Omit<TDocument, "_createdAt" | "_rev" | "_updatedAt"> & {
_type: string;
},
const Options extends BaseMutationOptions & {
returnDocuments?: boolean;
returnFirst?: boolean;
}
>(
document: Doc,
options?: Options
) => PromiseOrObservable<
TIsPromise,
Options extends { returnDocuments: false; returnFirst: false }
? MultipleMutationResult
: Options extends { returnFirst: false }
? Extract<TDocument, { _type: Doc["_type"] }>[]
: Options extends { returnDocuments: false }
? SingleMutationResult
: Extract<TDocument, { _type: Doc["_type"] }>
>;
createOrReplace: <
const Doc extends Omit<TDocument, "_createdAt" | "_rev" | "_updatedAt"> & {
_type: string;
},
const Options extends BaseMutationOptions & {
returnDocuments?: boolean;
returnFirst?: boolean;
}
>(
document: Doc,
options?: Options
) => PromiseOrObservable<
TIsPromise,
Options extends { returnDocuments: false; returnFirst: false }
? MultipleMutationResult
: Options extends { returnFirst: false }
? Extract<TDocument, { _type: Doc["_type"] }>[]
: Options extends { returnDocuments: false }
? SingleMutationResult
: Extract<TDocument, { _type: Doc["_type"] }>
>;
delete: <
const Options extends BaseMutationOptions & {
returnDocuments?: boolean;
returnFirst?: boolean;
}
>(
selection: MutationSelection | string,
options?: Options
) => PromiseOrObservable<
TIsPromise,
Options extends { returnDocuments: false; returnFirst: false }
? MultipleMutationResult
: Options extends { returnFirst: false }
? TDocument[]
: Options extends { returnDocuments: false }
? SingleMutationResult
: TDocument
>;
fetch: <
const Query extends string,
const Q = QueryParams,
Expand All @@ -73,7 +187,8 @@ export interface SanityClient<
query: Query,
params?: Q,
options?: Options
) => Promise<
) => PromiseOrObservable<
TIsPromise,
MaybeRawQueryResponse<
ExecuteQuery<
Query,
Expand All @@ -96,17 +211,43 @@ export interface SanityClient<
getDocument: <const Id extends string>(
id: Id,
options?: Parameters<SanityClientNative["getDocument"]>[1]
) => Promise<
) => PromiseOrObservable<
TIsPromise,
(TDocument extends never ? never : TDocument & { _id: Id }) | undefined
>;
getDocuments: <const Ids extends readonly string[]>(
ids: Ids,
options?: Parameters<SanityClientNative["getDocuments"]>[1]
) => Promise<GetDocuments<WritableDeep<Ids>, TDocument>>;
) => PromiseOrObservable<
TIsPromise,
GetDocuments<WritableDeep<Ids>, TDocument>
>;
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- Recursive type
observable: ObservableSanityClient<TClientConfig, TDocument>;
withConfig: <const NewConfig extends Partial<ClientConfig>>(
newConfig?: NewConfig
) => SanityClient<Merge<TClientConfig, NewConfig>, TDocument>;
}
) => OverrideSanityClient<
TSanityClient,
Merge<TClientConfig, NewConfig>,
TDocument,
TIsPromise
>;
};

export type ObservableSanityClient<
TClientConfig extends ClientConfig,
TDocument extends AnySanityDocument
> = OverrideSanityClient<
ObservableSanityClientNative,
TClientConfig,
TDocument,
false
>;

export type SanityClient<
TClientConfig extends ClientConfig,
TDocument extends AnySanityDocument
> = OverrideSanityClient<SanityClientNative, TClientConfig, TDocument, true>;

/**
* Unfortunately, this has to have a very weird function signature due to this typescript issue:
Expand Down
Loading

0 comments on commit 4e3e8a9

Please sign in to comment.