From eda021d8c4e50fb4c0b910394378b7335ffac42a Mon Sep 17 00:00:00 2001 From: Dan Greaves Date: Thu, 4 Apr 2024 13:13:11 +1100 Subject: [PATCH] feat: add reference utility type --- src/schemas/ConditionalUnion.ts | 11 ++++++- src/schemas/Projection.ts | 11 ++++++- src/schemas/Reference.test.ts | 58 +++++++++++++++++++++++++++++++++ src/schemas/Reference.ts | 25 ++++++++++++++ src/schemas/index.ts | 2 ++ 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/schemas/Reference.test.ts create mode 100644 src/schemas/Reference.ts diff --git a/src/schemas/ConditionalUnion.ts b/src/schemas/ConditionalUnion.ts index 6b65ff9..f03bdd0 100644 --- a/src/schemas/ConditionalUnion.ts +++ b/src/schemas/ConditionalUnion.ts @@ -1,4 +1,4 @@ -import { Type, TSchema, TUnion } from "@sinclair/typebox"; +import { Type, TSchema, TUnion, TypeGuard } from "@sinclair/typebox"; /** * Options available when creating a conditional union. @@ -27,6 +27,7 @@ type SchemaArrayFromConditions> = export type TConditionalUnion< T extends Record = Record, > = TUnion> & { + groqType: "conditionalUnion"; [optionsKey]?: TConditionalUnionOptions; [originalConditionsKey]: T; }; @@ -79,12 +80,20 @@ export function ConditionalUnion< const schema = Type.Union(schemas, { groq }) as TConditionalUnion; // Attach additional attributes. + schema.groqType = "conditionalUnion"; if (options) schema[optionsKey] = options; schema[originalConditionsKey] = conditions; return schema; } +/** + * Return true if the given schema is a conditional union. + */ +export function isConditionalUnion(value: unknown): value is TConditionalUnion { + return TypeGuard.IsSchema(value) && "conditionalUnion" === value.groqType; +} + /** * Expand the given conditional union. */ diff --git a/src/schemas/Projection.ts b/src/schemas/Projection.ts index 325db54..8d4449f 100644 --- a/src/schemas/Projection.ts +++ b/src/schemas/Projection.ts @@ -1,4 +1,4 @@ -import { Type, TObject, TProperties } from "@sinclair/typebox"; +import { Type, TObject, TProperties, TypeGuard } from "@sinclair/typebox"; /** * Options available when creating a projection. @@ -21,6 +21,7 @@ const originalPropertiesKey = Symbol("originalProperties"); * Fetch a single object projection. */ export type TProjection = TObject & { + groqType: "projection"; [optionsKey]?: TProjectionOptions; [originalPropertiesKey]: T; }; @@ -83,12 +84,20 @@ export function Projection( const schema = Type.Object(properties, { groq }) as TProjection; // Attach additional attributes. + schema["groqType"] = "projection"; if (options) schema[optionsKey] = options; schema[originalPropertiesKey] = properties; return schema; } +/** + * Return true if the given schema is a projection. + */ +export function isProjection(value: unknown): value is TProjection { + return TypeGuard.IsSchema(value) && "projection" === value.groqType; +} + /** * Expand the given projection. */ diff --git a/src/schemas/Reference.test.ts b/src/schemas/Reference.test.ts new file mode 100644 index 0000000..ccc9ae3 --- /dev/null +++ b/src/schemas/Reference.test.ts @@ -0,0 +1,58 @@ +import { expect, test, describe } from "vitest"; + +import * as S from "./index"; + +describe("projection", () => { + test("clones and expands the projection", () => { + const schema = S.Projection({ + name: S.String(), + email: S.String(), + }); + + const expandedSchema = S.Reference(schema); + + expect(schema.groq).toBe(`{name,email}`); + + expect(expandedSchema.groq).toBe(`{...@->{name,email}}`); + }); + + test("clones and expands the projection with a conditional", () => { + const schema = S.Projection({ + name: S.String(), + email: S.String(), + }); + + const expandedSchema = S.Reference(schema, "reference"); + + expect(schema.groq).toBe(`{name,email}`); + + expect(expandedSchema.groq).toBe( + `{_type == "reference" => @->{name,email},_type != "reference" => @{name,email}}`, + ); + }); +}); + +describe("typed union", () => { + test("clones and expands the projection", () => { + const schema = S.TypedUnion([ + S.TypedProjection({ + _type: S.Literal("person"), + name: S.String(), + }), + S.TypedProjection({ + _type: S.Literal("company"), + companyName: S.String(), + }), + ]); + + const expandedSchema = S.Reference(schema); + + expect(schema.groq).toBe( + `{...select(_type == "person" => {_type,name},_type == "company" => {_type,companyName},{"_rawType":_type,"_type":"unknown"})}`, + ); + + expect(expandedSchema.groq).toBe( + `{...@->{...select(_type == "person" => {_type,name},_type == "company" => {_type,companyName},{"_rawType":_type,"_type":"unknown"})}}`, + ); + }); +}); diff --git a/src/schemas/Reference.ts b/src/schemas/Reference.ts new file mode 100644 index 0000000..4c50fdb --- /dev/null +++ b/src/schemas/Reference.ts @@ -0,0 +1,25 @@ +import { isProjection, expandProjection, type TProjection } from "./Projection"; + +import { + isConditionalUnion, + expandConditionalUnion, + type TConditionalUnion, +} from "./ConditionalUnion"; + +/** + * Dereference the given schema. + */ +export function Reference( + schema: T, + conditionalAttribute?: string, +): T { + if (isProjection(schema)) { + return expandProjection(schema, conditionalAttribute); + } + + if (isConditionalUnion(schema)) { + return expandConditionalUnion(schema, conditionalAttribute); + } + + return schema; +} diff --git a/src/schemas/index.ts b/src/schemas/index.ts index c468813..e6e0784 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -4,6 +4,7 @@ declare module "@sinclair/typebox" { interface TSchema { groq?: string; + groqType?: string; } } @@ -24,5 +25,6 @@ export * from "./ConditionalUnion"; export * from "./Nullable"; export * from "./Projection"; export * from "./Raw"; +export * from "./Reference"; export * from "./TypedProjection"; export * from "./TypedUnion";