diff --git a/packages/groq/src/index.test.ts b/packages/groq/src/index.test.ts index 2c4d521b..27229431 100644 --- a/packages/groq/src/index.test.ts +++ b/packages/groq/src/index.test.ts @@ -5,60 +5,52 @@ import type { DocumentValue } from "@sanity-typed/types"; import type { ExecuteQuery, Scope } from "."; +type Empty = { [key: string]: never }; + describe("groq", () => { it("null", () => - expectType< - ExecuteQuery<"null", { [key: string]: never }> - >().toStrictEqual()); + expectType>().toStrictEqual()); it("true", () => - expectType< - ExecuteQuery<"true", { [key: string]: never }> - >().toStrictEqual()); + expectType>().toStrictEqual()); it("false", () => - expectType< - ExecuteQuery<"false", { [key: string]: never }> - >().toStrictEqual()); + expectType>().toStrictEqual()); it("-5.6", () => - expectType< - ExecuteQuery<"-5.6", { [key: string]: never }> - >().toStrictEqual<-5.6>()); + expectType>().toStrictEqual<-5.6>()); it('"double quoted string"', () => expectType< - ExecuteQuery<'"double quoted string"', { [key: string]: never }> + ExecuteQuery<'"double quoted string"', Empty> >().toStrictEqual<"double quoted string">()); it("'single quoted string'", () => expectType< - ExecuteQuery<"'single quoted string'", { [key: string]: never }> + ExecuteQuery<"'single quoted string'", Empty> >().toStrictEqual<"single quoted string">()); - it("[]", () => - expectType>().toStrictEqual< - [] - >()); + it("[]", () => expectType>().toStrictEqual<[]>()); it("[null,true,false,-5.6,\"double quoted string\",'single quoted string']", () => expectType< ExecuteQuery< "[null,true,false,-5.6,\"double quoted string\",'single quoted string']", - { [key: string]: never } + Empty > >().toStrictEqual< [null, true, false, -5.6, "double quoted string", "single quoted string"] >()); + it("[1,notvalid]", () => + expectType().toStrictEqual>()); + it("[...[null]]", () => - expectType< - ExecuteQuery<"[...[null]]", { [key: string]: never }> - >().toStrictEqual<[null]>()); + expectType>().toStrictEqual<[null]>()); it("[[null,null],[null,null,null]]", () => expectType< - ExecuteQuery<"[[null,null],[null,null,null]]", { [key: string]: never }> + ExecuteQuery<"[[null,null],[null,null,null]]", Empty> >().toStrictEqual<[[null, null], [null, null, null]]>()); it("@", () => { @@ -136,10 +128,24 @@ describe("groq", () => { >().toStrictEqual(); }); - it("(*)", () => + it("(10)", () => + expectType>().toStrictEqual<10>()); + + it("(((10)))", () => + expectType>().toStrictEqual<10>()); + + it("false[@]", () => + expectType>().toStrictEqual()); + + it("[true,false][@]", () => + expectType>().toStrictEqual< + [true] + >()); + + it("*[true]", () => expectType< ExecuteQuery< - "(*)", + "*[true]", { bar: DocumentValue<"bar", never>; foo: DocumentValue<"foo", never>; @@ -150,53 +156,67 @@ describe("groq", () => { (DocumentValue<"bar", never> | DocumentValue<"foo", never>)[] >()); - it("count(5)", () => + it('*[_type=="foo"]', () => expectType< - ExecuteQuery<"count(5)", { [key: string]: never }> - >().toStrictEqual()); + ExecuteQuery< + '*[_type=="foo"]', + { + bar: DocumentValue<"bar", never>; + foo: DocumentValue<"foo", never>; + qux: { _type: "qux" }; + } + > + >().toStrictEqual[]>()); + + it("4==5", () => + expectType>().toStrictEqual()); + + it("4!=5", () => + expectType>().toStrictEqual()); + + it("5==5", () => + expectType>().toStrictEqual()); + + it("5!=5", () => + expectType>().toStrictEqual()); + + it("count(5)", () => + expectType>().toStrictEqual()); it("count([1,2,3,4])", () => - expectType< - ExecuteQuery<"count([1,2,3,4])", { [key: string]: never }> - >().toStrictEqual<4>()); + expectType>().toStrictEqual<4>()); it("global::count([1,2,3,4])", () => expectType< - ExecuteQuery<"global::count([1,2,3,4])", { [key: string]: never }> + ExecuteQuery<"global::count([1,2,3,4])", Empty> >().toStrictEqual<4>()); it("defined(null)", () => - expectType< - ExecuteQuery<"defined(null)", { [key: string]: never }> - >().toStrictEqual()); + expectType>().toStrictEqual()); it("defined(5)", () => - expectType< - ExecuteQuery<"defined(5)", { [key: string]: never }> - >().toStrictEqual()); + expectType>().toStrictEqual()); it("global::defined(5)", () => expectType< - ExecuteQuery<"global::defined(5)", { [key: string]: never }> + ExecuteQuery<"global::defined(5)", Empty> >().toStrictEqual()); it("length(10)", () => - expectType< - ExecuteQuery<"length(10)", { [key: string]: never }> - >().toStrictEqual()); + expectType>().toStrictEqual()); it("length([null,null,null])", () => expectType< - ExecuteQuery<"length([null,null,null])", { [key: string]: never }> + ExecuteQuery<"length([null,null,null])", Empty> >().toStrictEqual<3>()); it('length("string")', () => expectType< - ExecuteQuery<'length("string")', { [key: string]: never }> + ExecuteQuery<'length("string")', Empty> >().toStrictEqual()); it('global::length("string")', () => expectType< - ExecuteQuery<'global::length("string")', { [key: string]: never }> + ExecuteQuery<'global::length("string")', Empty> >().toStrictEqual()); }); diff --git a/packages/groq/src/index.ts b/packages/groq/src/index.ts index 8f01d882..e1495066 100644 --- a/packages/groq/src/index.ts +++ b/packages/groq/src/index.ts @@ -23,6 +23,39 @@ export type Scope< this: Value; }; +type NestedScope< + Value, + TScope extends Scope +> = TScope extends Scope + ? Scope + : never; + +/** + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Evaluate() + */ +type Evaluate> = + // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Evaluate should be at the bottom but, because of recursion, it's cleanest to put it here + Expression; + +/** + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Boolean + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Null + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Number + */ +type Primitives = + TExpression extends `${infer TBoolean extends boolean | number | null}` + ? TBoolean + : never; + +/** + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#String + */ +type StringType = TExpression extends // FIXME Should check for specific characters + | `'${infer TString extends string}'` + | `"${infer TString extends string}"` + ? TString + : never; + /** * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#ArrayElement */ @@ -30,18 +63,10 @@ type ArrayElement< TExpression extends string, TScope extends Scope > = TExpression extends `...${infer TArrayElement}` - ? // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate extends any[] - ? // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate - : [ - // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate - ] - : [ - // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate - ]; + ? Evaluate extends any[] + ? Evaluate + : [Evaluate] + : [Evaluate]; /** * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#ArrayElements @@ -53,10 +78,14 @@ type ArrayElements< > = TExpression extends `${infer TArrayElement},${infer TArrayElements}` ? ArrayElement<`${TPrefix}${TArrayElement}`, TScope> extends [never] ? ArrayElements + : ArrayElements extends never + ? never : [ ...ArrayElement<`${TPrefix}${TArrayElement}`, TScope>, ...ArrayElements ] + : ArrayElement<`${TPrefix}${TExpression}`, TScope> extends [never] + ? never : ArrayElement<`${TPrefix}${TExpression}`, TScope>; /** @@ -71,25 +100,6 @@ type ArrayType< : ArrayElements : never; -/** - * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#String - */ -type StringType = TExpression extends // FIXME Should check for specific characters - | `'${infer TString extends string}'` - | `"${infer TString extends string}"` - ? TString - : never; - -/** - * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Boolean - * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Null - * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Number - */ -type Primitives = - TExpression extends `${infer TBoolean extends boolean | number | null}` - ? TBoolean - : never; - /** * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Literal * @@ -100,43 +110,82 @@ type Literal> = | Primitives | StringType; +type Negate = TBoolean extends true ? false : true; + /** - * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#sec-global-count- + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Equality + * + * @todo Test Equality cases in https://sanity-io.github.io/GROQ/GROQ-1.revision1/#PartialCompare() */ -type Count> = - // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate extends never +type Equality< + TExpression extends string, + TScope extends Scope +> = TExpression extends `${infer TLeft}!=${infer TRight}` + ? Negate> + : TExpression extends `${infer TLeft}==${infer TRight}` + ? Evaluate extends never ? never - : // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate extends any[] - ? // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate["length"] - : null; + : Evaluate extends never + ? never + : Evaluate extends Evaluate + ? true + : Evaluate extends Evaluate + ? true + : false + : never; + +/** + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#OperatorCall + * + * @todo And + * @todo Asc + * @todo Comparison + * @todo Desc + * @todo In + * @todo Match + * @todo Minus + * @todo Not + * @todo Or + * @todo Percent + * @todo Plus + * @todo Slash + * @todo Star + * @todo StarStar + * @todo UnaryMinus + * @todo UnaryPlus + */ +type OperatorCall< + TExpression extends string, + TScope extends Scope +> = Equality; + +/** + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#sec-global-count- + */ +type Count< + TArgs extends string, + TScope extends Scope +> = Evaluate extends any[] + ? Evaluate["length"] + : null; /** * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#sec-global-defined- */ -type Defined> = - // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate extends never - ? never - : // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate extends null - ? false - : true; +type Defined< + TArgs extends string, + TScope extends Scope +> = Evaluate extends null ? false : true; /** * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#sec-global-length- */ -type Length> = - // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate extends never - ? never - : // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate extends any[] | string - ? // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate["length"] - : null; +type Length< + TArgs extends string, + TScope extends Scope +> = Evaluate extends any[] | string + ? Evaluate["length"] + : null; /** * @todo array @@ -259,6 +308,81 @@ type SimpleExpression< | This | ThisAttribute; +/** + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Filter + */ +type Filter< + TBase extends any[], + TExpression extends string, + TScope extends Scope +> = TBase extends [] + ? [] + : TBase extends [infer TFirst, ...infer TRest] + ? Evaluate> extends true + ? [TFirst, ...Filter] + : Filter + : TBase extends (infer ArrayElement)[] + ? (ArrayElement extends never + ? never + : Evaluate> extends true + ? ArrayElement + : never)[] + : []; + +type BasicTraversalFilter< + TExpression extends string, + TScope extends Scope, + TPrefix extends string = "" +> = TExpression extends `${infer TBase}[${infer TFilterExpression}]` + ? Evaluate<`${TPrefix}${TBase}`, TScope> extends never + ? BasicTraversalFilter< + `${TFilterExpression}]`, + TScope, + `${TPrefix}${TBase}[` + > + : Evaluate<`${TPrefix}${TBase}`, TScope> extends any[] + ? Filter, TFilterExpression, TScope> + : Evaluate<`${TPrefix}${TBase}`, TScope> + : never; + +/** + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#BasicTraversalArray + * + * @todo Slice + * @todo ArrayPostfix + */ +type BasicTraversalArray< + TExpression extends string, + TScope extends Scope +> = BasicTraversalFilter; + +/** + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#TraversalArray + * + * @todo BasicTraversalArray TraversalArray + * @todo ElementAccess TraversalArray + * @todo ElementAccess TraversalArrayTarget + * @todo BasicTraversalArray TraversalPlain + * @todo BasicTraversalArray TraversalArrayTarget + * @todo Projection TraversalArray + */ +type TraversalArray< + TExpression extends string, + TScope extends Scope +> = BasicTraversalArray; + +/** + * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#TraversalExpression + * + * @todo TraversalPlain + * @todo TraversalArraySource + * @todo TraversalArrayTarget + */ +type TraversalExpression< + TExpression extends string, + TScope extends Scope +> = TraversalArray; + /** * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Parenthesis */ @@ -266,25 +390,21 @@ type Parenthesis< TExpression extends string, TScope extends Scope > = TExpression extends `(${infer TInnerExpression})` - ? // eslint-disable-next-line @typescript-eslint/no-use-before-define -- recursion - Evaluate + ? Evaluate : never; /** * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#CompoundExpression * * @todo PipeFuncCall - * @todo TraversalExpression */ type CompoundExpression< TExpression extends string, TScope extends Scope -> = Parenthesis; +> = Parenthesis | TraversalExpression; /** * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Expression - * - * @todo OperatorCall */ type Expression< TExpression extends string, @@ -292,16 +412,9 @@ type Expression< > = | CompoundExpression | Literal + | OperatorCall | SimpleExpression; -/** - * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#Evaluate() - */ -type Evaluate< - TExpression extends string, - TScope extends Scope -> = Expression; - /** * @link https://sanity-io.github.io/GROQ/GROQ-1.revision1/#ExecuteQuery() */