Skip to content

Commit

Permalink
feat(nest): port makeEnum from cord (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
CarsonF authored Mar 5, 2024
1 parent 764637d commit a29648e
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 64 deletions.
2 changes: 2 additions & 0 deletions packages/nest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
"dependencies": {
"@nestjs/common": "^10",
"@nestjs/core": "^10",
"@nestjs/graphql": "^12",
"@seedcompany/common": ">0.3 <1"
},
"peerDependencies": {
"@nestjs/common": "^10",
"@nestjs/core": "^10",
"@nestjs/graphql": "^12",
"reflect-metadata": "^0.1.12"
},
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions packages/nest/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './make-enum';
export * from './repl';
198 changes: 198 additions & 0 deletions packages/nest/src/make-enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { registerEnumType } from '@nestjs/graphql';
import { cleanJoin, nonEnumerable, setHas } from '@seedcompany/common';
import { inspect, InspectOptionsStylized } from 'util';

export type EnumType<Enum> = Enum extends MadeEnum<infer Values, any, any>
? Values
: never;

export type MadeEnum<
Values extends string,
Extra = unknown,
ValueDeclaration = EnumValueDeclarationShape,
> = {
readonly [Value in Values & string]: Value;
} & Readonly<Extra> &
EnumHelpers<Values, ValueDeclaration>;

interface EnumOptions<
ValueDeclaration extends EnumValueDeclarationShape,
Extra extends Record<string, any>,
> {
/**
* The name to register this enum with GraphQL.
* If this is omitted, the enum is not exposed to GraphQL.
*/
readonly name?: string;

/**
* The description of this enum for GraphQL.
*/
readonly description?: string;

/**
* The values/members of the enum.
* These can be strings or objects with extra metadata.
*/
readonly values: readonly ValueDeclaration[];

/**
* Expose the order of the enum values to GraphQL.
*/
readonly exposeOrder?: boolean;

/**
* Add extra (non-enumerable) properties to this enum object.
* These will not be values of the enum, but helper things.
*
* This is given the built enum (without any extras), to prevent circular references.
*/
readonly extra?: (
enumObject: MadeEnum<
ValuesOfDeclarations<ValueDeclaration>,
unknown,
NormalizedValueDeclaration<ValueDeclaration>
>,
) => Extra;
}

/**
* Create a better enum object that can be used in both TS & GraphQL.
*/
export const makeEnum = <
const ValueDeclaration extends EnumValueDeclarationShape,
const Extra extends Record<string, any> = never,
>(
input: EnumOptions<ValueDeclaration, Extra>,
): MadeEnum<
ValuesOfDeclarations<ValueDeclaration>,
[Extra] extends [never] ? unknown : Extra,
NormalizedValueDeclaration<ValueDeclaration>
> => {
const {
name,
description,
values: valuesIn,
exposeOrder,
extra: extraFn,
} = input;

const entries = valuesIn.map(
(value: EnumValueDeclarationShape): EnumValueDeclarationObjectShape =>
typeof value === 'string' ? { value } : value,
);

const object = Object.fromEntries(entries.map((v) => [v.value, v.value]));

const valueList = Object.keys(object);
const values = new Set(valueList);
const helpers = {
values,
entries,
[Symbol.iterator]: () => values.values(),
// @ts-expect-error Ignoring generics for implementation.
has: (value: string) => setHas(values, value),
[inspect.custom]: (
depth: number,
options: InspectOptionsStylized,
innerInspect: typeof inspect,
) => {
const label = options.stylize(
`[Enum${name ? `: ${name}` : ''}]`,
'special',
);
if (depth <= 0) {
return label;
}
const members = innerInspect(valueList).slice(1, -1).replace(/'/g, '');
return `${label} {${members}}`;
},
} satisfies EnumHelpers<string, any>;

Object.assign(object, helpers);
nonEnumerable(object, ...Object.keys(helpers));

if (extraFn) {
const extra = extraFn(object as any);
Object.assign(object, extra);
nonEnumerable(object, ...Object.keys(extra));
}

if (name) {
const valuesMap = Object.fromEntries(
entries.map((v, i) => [
v.value,
{
deprecationReason: v.deprecationReason,
description:
cleanJoin('\n\n', [
v.description,
v.label ? `@label ${v.label}` : undefined,
exposeOrder ? `@order ${i}` : undefined,
]) || undefined,
},
]),
);
registerEnumType(object, { name, description, valuesMap });
}

return object as any;
};

type EnumValueDeclarationShape<Value extends string = string> =
| Value
| EnumValueDeclarationObjectShape<Value>;

interface EnumValueDeclarationObjectShape<Value extends string = string> {
/**
* The actual value.
*/
readonly value: Value;
/**
* Declare a custom label for this value which is exposed in GraphQL schema.
*/
readonly label?: string;
/**
* Declare a description for this value which is exposed in GraphQL schema.
*/
readonly description?: string;
/**
* Declare this value as deprecated with the given reason, exposed to GraphQL schema.
*/
readonly deprecationReason?: string;
}

type ValuesOfDeclarations<ValueDeclaration extends EnumValueDeclarationShape> =
ValueDeclaration extends string
? ValueDeclaration
: ValueDeclaration extends EnumValueDeclarationObjectShape<infer Value>
? Value
: never;

/**
* This unifies all values to have the standard object shape, plus the extra
* properties as optional.
*/
type NormalizedValueDeclaration<Declaration extends EnumValueDeclarationShape> =
// For values that are objects, accept them as they are...
| (Extract<Declaration, EnumValueDeclarationObjectShape> &
// plus all the normal object keys
EnumValueDeclarationObjectShape<ValuesOfDeclarations<Declaration>>)
// For values that are strings, convert them to the standard shape...
| (EnumValueDeclarationObjectShape<Extract<Declaration, string>> &
// and include all the extra keys as optional
Partial<
Omit<
Extract<Declaration, EnumValueDeclarationObjectShape>,
keyof EnumValueDeclarationObjectShape
>
>);

interface EnumHelpers<Values extends string, ValueDeclaration> {
readonly values: ReadonlySet<Values>;
readonly entries: ReadonlyArray<Readonly<ValueDeclaration>>;
readonly has: <In extends string>(
value: In & {},
) => value is In & Values & {};
readonly [Symbol.iterator]: () => Iterator<Values>;
}
Loading

0 comments on commit a29648e

Please sign in to comment.