Skip to content

Commit

Permalink
feat: add reference utility type
Browse files Browse the repository at this point in the history
  • Loading branch information
dangreaves committed Apr 4, 2024
1 parent 358fa64 commit eda021d
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/schemas/ConditionalUnion.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -27,6 +27,7 @@ type SchemaArrayFromConditions<T extends Record<string, TSchema>> =
export type TConditionalUnion<
T extends Record<string, TSchema> = Record<string, TSchema>,
> = TUnion<SchemaArrayFromConditions<T>> & {
groqType: "conditionalUnion";
[optionsKey]?: TConditionalUnionOptions;
[originalConditionsKey]: T;
};
Expand Down Expand Up @@ -79,12 +80,20 @@ export function ConditionalUnion<
const schema = Type.Union(schemas, { groq }) as TConditionalUnion<T>;

// 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.
*/
Expand Down
11 changes: 10 additions & 1 deletion src/schemas/Projection.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -21,6 +21,7 @@ const originalPropertiesKey = Symbol("originalProperties");
* Fetch a single object projection.
*/
export type TProjection<T extends TProperties = TProperties> = TObject<T> & {
groqType: "projection";
[optionsKey]?: TProjectionOptions;
[originalPropertiesKey]: T;
};
Expand Down Expand Up @@ -83,12 +84,20 @@ export function Projection<T extends TProperties = TProperties>(
const schema = Type.Object(properties, { groq }) as TProjection<T>;

// 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.
*/
Expand Down
58 changes: 58 additions & 0 deletions src/schemas/Reference.test.ts
Original file line number Diff line number Diff line change
@@ -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"})}}`,
);
});
});
25 changes: 25 additions & 0 deletions src/schemas/Reference.ts
Original file line number Diff line number Diff line change
@@ -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<T extends TProjection | TConditionalUnion>(
schema: T,
conditionalAttribute?: string,
): T {
if (isProjection(schema)) {
return expandProjection(schema, conditionalAttribute);
}

if (isConditionalUnion(schema)) {
return expandConditionalUnion(schema, conditionalAttribute);
}

return schema;
}
2 changes: 2 additions & 0 deletions src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
declare module "@sinclair/typebox" {
interface TSchema {
groq?: string;
groqType?: string;
}
}

Expand All @@ -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";

0 comments on commit eda021d

Please sign in to comment.