Skip to content

Commit

Permalink
feature(conditionals): added an "exhaustive" check for conditionals
Browse files Browse the repository at this point in the history
  • Loading branch information
scottrippey committed Jan 17, 2024
1 parent 90d612f commit 29529ab
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 39 deletions.
1 change: 1 addition & 0 deletions packages/groq-builder/src/commands/conditional$.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("conditional$", () => {
msrp: true,
},
});

it("we should be able to extract the intersection of projection types", () => {
expectType<
Simplify<ExtractConditionalProjectionTypes<typeof conditionalResult>>
Expand Down
38 changes: 24 additions & 14 deletions packages/groq-builder/src/commands/conditional$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
ConditionalKey,
ConditionalProjectionMap,
ExtractConditionalProjectionResults,
SpreadableConditionals,
} from "./conditional-types";
import { notNull } from "../types/utils";
import { ParserFunction } from "../types/public-types";
Expand All @@ -19,23 +18,28 @@ declare module "../groq-builder" {
ResultItem<TResult>,
TRootConfig
>,
TKey extends string = "[$]"
TKey extends string = "[$]",
TIsExhaustive extends boolean = false
>(
conditionalProjections: TConditionalProjections,
config?: ConditionalConfig<TKey>
config?: Partial<ConditionalConfig<TKey, TIsExhaustive>>
): ExtractConditionalProjectionResults<
ResultItem<TResult>,
TConditionalProjections,
TKey
ConditionalConfig<TKey, TIsExhaustive>
>;
}
}

GroqBuilder.implement({
conditional$<TCP extends object, TKey extends string>(
conditional$<
TCP extends object,
TKey extends string,
TIsExhaustive extends boolean
>(
this: GroqBuilder,
conditionalProjections: TCP,
config?: ConditionalConfig<TKey>
config?: Partial<ConditionalConfig<TKey, TIsExhaustive>>
) {
const root = this.root;
const allConditionalProjections = Object.entries(
Expand All @@ -58,20 +62,21 @@ GroqBuilder.implement({
.filter(notNull);
const conditionalParser = !parsers.length
? null
: createConditionalParserUnion(parsers);
: createConditionalParserUnion(parsers, config?.isExhaustive ?? false);

const conditionalQuery = root.chain(query, conditionalParser);
const uniqueKey: ConditionalKey<TKey> = `[Conditional] ${
config?.key ?? ("[$]" as TKey)
}`;

const key = config?.key || ("[$]" as TKey);
const conditionalKey: ConditionalKey<TKey> = `[Conditional] ${key}`;
return {
[uniqueKey]: conditionalQuery,
} as unknown as SpreadableConditionals<TKey, any>;
[conditionalKey]: conditionalQuery,
} as any;
},
});

function createConditionalParserUnion(parsers: ParserFunction[]) {
function createConditionalParserUnion(
parsers: ParserFunction[],
isExhaustive: boolean
) {
return function parserUnion(input: unknown) {
for (const parser of parsers) {
try {
Expand All @@ -82,6 +87,11 @@ function createConditionalParserUnion(parsers: ParserFunction[]) {
// or if it errored due to not meeting the conditional check.
}
}
if (isExhaustive) {
throw new TypeError(
`The data did not match any of the ${parsers.length} conditional assertions`
);
}
return {};
};
}
33 changes: 19 additions & 14 deletions packages/groq-builder/src/commands/conditional-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ export type ConditionalExpression<TResultItem> = Tagged<string, TResultItem>;
export type ExtractConditionalProjectionResults<
TResultItem,
TConditionalProjectionMap extends ConditionalProjectionMap<any, any>,
TKey extends string
TConfig extends ConditionalConfig
> = SpreadableConditionals<
TKey,
| Empty
TConfig["key"],
| (TConfig["isExhaustive"] extends true ? never : Empty)
| ValueOf<{
[P in keyof TConditionalProjectionMap]: ExtractProjectionResult<
TResultItem,
Expand All @@ -47,13 +47,12 @@ export type OmitConditionalProjections<TResultItem> = {
};

export type ExtractConditionalProjectionTypes<TProjectionMap> = Simplify<
| Empty
| IntersectionOfValues<{
[P in Extract<
keyof TProjectionMap,
ConditionalKey<string>
>]: InferResultType<Extract<TProjectionMap[P], IGroqBuilder>>;
}>
IntersectionOfValues<{
[P in Extract<
keyof TProjectionMap,
ConditionalKey<string>
>]: InferResultType<Extract<TProjectionMap[P], IGroqBuilder>>;
}>
>;

export type ConditionalByTypeProjectionMap<
Expand All @@ -72,10 +71,10 @@ export type ExtractConditionalByTypeProjectionResults<
any,
any
>,
TKey extends string
TConfig extends ConditionalConfig
> = SpreadableConditionals<
TKey,
| Empty
TConfig["key"],
| (TConfig["isExhaustive"] extends true ? never : Empty)
| ValueOf<{
[_type in keyof TConditionalByTypeProjectionMap]: ExtractProjectionResult<
Extract<TResultItem, { _type: _type }>,
Expand All @@ -99,4 +98,10 @@ export type SpreadableConditionals<
[UniqueConditionalKey in ConditionalKey<TKey>]: IGroqBuilder<ConditionalResultType>;
};

export type ConditionalConfig<TKey> = { key: TKey };
export type ConditionalConfig<
TKey extends string = string,
TIsExhaustive extends boolean = boolean
> = {
key: TKey;
isExhaustive: TIsExhaustive;
};
18 changes: 18 additions & 0 deletions packages/groq-builder/src/commands/conditionalByType.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ describe("conditionalByType", () => {
});
});

it("types are correct when the conditions are exhaustive", () => {
const conditionsExhaustive = q.star
.filterByType("product", "variant")
.conditionalByType({
product: { _type: true, name: true },
variant: { _type: true, price: true },
});

type ActualItem = ExtractConditionalProjectionTypes<
typeof conditionsExhaustive
>;
type ExpectedItem =
| { _type: "product"; name: string }
| { _type: "variant"; price: number };

expectType<Simplify<ActualItem>>().toStrictEqual<ExpectedItem>();
});

it("should be able to extract the return types", () => {
type ConditionalResults = ExtractConditionalProjectionTypes<
typeof conditionalByType
Expand Down
33 changes: 22 additions & 11 deletions packages/groq-builder/src/commands/conditionalByType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
ExtractConditionalByTypeProjectionResults,
ConditionalByTypeProjectionMap,
ConditionalKey,
SpreadableConditionals,
ConditionalConfig,
} from "./conditional-types";
import { ProjectionMap } from "./projection-types";
import { IsEqual } from "../tests/expectType";

declare module "../groq-builder" {
export interface GroqBuilder<TResult, TRootConfig> {
Expand All @@ -17,26 +17,32 @@ declare module "../groq-builder" {
ResultItem<TResult>,
TRootConfig
>,
TKey extends string = "[ByType]"
TKey extends string = "[ByType]",
TIsExhaustive extends boolean = TConditionalProjections extends Required<
ConditionalByTypeProjectionMap<ResultItem<TResult>, TRootConfig>
>
? true
: false
>(
conditionalProjections: TConditionalProjections,
config?: ConditionalConfig<TKey>
config?: Partial<ConditionalConfig<TKey, TIsExhaustive>>
): ExtractConditionalByTypeProjectionResults<
ResultItem<TResult>,
TConditionalProjections,
TKey
ConditionalConfig<TKey, TIsExhaustive>
>;
}
}

GroqBuilder.implement({
conditionalByType<
TConditionalProjections extends object,
TKey extends string
TKey extends string,
TIsExhaustive extends boolean
>(
this: GroqBuilder<any, RootConfig>,
conditionalProjections: TConditionalProjections,
config?: ConditionalConfig<TKey>
config?: Partial<ConditionalConfig<TKey, TIsExhaustive>>
) {
const typeNames = Object.keys(conditionalProjections);

Expand All @@ -63,15 +69,20 @@ GroqBuilder.implement({
if (typeParser?.parser) {
return typeParser.parser(input);
}
if (!typeParser && config?.isExhaustive) {
throw new TypeError(
`Unexpected _type "${input._type}"; expected one of: ${typeNames}`
);
}
return {};
};

const conditionalQuery = this.root.chain(query, conditionalParser);
const uniqueKey: ConditionalKey<string> = `[Conditional] ${
config?.key ?? ("[ByType]" as TKey)
}`;
const key: TKey = config?.key || ("[ByType]" as TKey);
const conditionalKey: ConditionalKey<TKey> = `[Conditional] ${key}`;
return {
[uniqueKey]: conditionalQuery,
} as unknown as SpreadableConditionals<TKey, any>;
_type: true, // Ensure we request the `_type` parameter
[conditionalKey]: conditionalQuery,
} as any;
},
});

0 comments on commit 29529ab

Please sign in to comment.