Skip to content

Commit 33c091c

Browse files
authored
Merge pull request #45 from mizdra/non-optional-fields
Allow omitting fields to be passed to `defaultFields`
2 parents 57d0d14 + 3f9c4a7 commit 33c091c

13 files changed

+129
-31
lines changed

e2e/1-basic-schema.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,9 @@ type NamingConventionTest_Type {
8686
type NamingConventionTest_SubType {
8787
field: String!
8888
}
89+
90+
# NonOptionalFields
91+
type NonOptionalFields_OptionalFieldsType {
92+
field1: String!
93+
field2: String!
94+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type NonOptionalFields_NonOptionalFieldsType {
2+
field1: String!
3+
field2: String!
4+
}

e2e/codegen.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@ const config: CodegenConfig = {
6969
typesSuffix: 'Suffix',
7070
},
7171
},
72+
'__generated__/4-non-optional-fields/types.ts': {
73+
schema: './4-non-optional-fields-schema.graphql',
74+
plugins: ['typescript'],
75+
config: {
76+
...defaultTypeScriptPluginConfig,
77+
},
78+
},
79+
'./__generated__/4-non-optional-fields/fabbrica.ts': {
80+
schema: './4-non-optional-fields-schema.graphql',
81+
plugins: ['@mizdra/graphql-fabbrica'],
82+
config: {
83+
...defaultFabbricaPluginConfig,
84+
nonOptionalFields: true,
85+
},
86+
},
7287
},
7388
};
7489

e2e/index.e2e.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import {
2121
defineNamingConventionTest_RenamedTypeFactory,
2222
defineNullableTest_TypeFactory,
2323
defineInputTest_InputFactory,
24+
defineNonOptionalFields_OptionalFieldsTypeFactory,
2425
} from './__generated__/1-basic/fabbrica.js';
2526
import { oneOf } from './test/util.js';
2627
import { definePrefixTypeFactory } from './__generated__/2-typesPrefix/fabbrica.js';
2728
import { defineTypeSuffixFactory } from './__generated__/3-typesSuffix/fabbrica.js';
29+
import { defineNonOptionalFields_NonOptionalFieldsTypeFactory } from './__generated__/4-non-optional-fields/fabbrica.js';
2830

2931
describe('integration test', () => {
3032
it('circular dependent type', async () => {
@@ -408,6 +410,34 @@ describe('defineTypeFactory', () => {
408410
expect(firstNameResolver).toHaveBeenCalledTimes(1);
409411
expect(lastNameResolver).toHaveBeenCalledTimes(1);
410412
});
413+
describe('nonOptionalFields', () => {
414+
it('requires to pass all fields if nonOptionalFields is false', async () => {
415+
defineNonOptionalFields_NonOptionalFieldsTypeFactory({
416+
// @ts-expect-error -- expects error
417+
defaultFields: {
418+
field1: 'field1',
419+
// field2: 'field2',
420+
},
421+
});
422+
});
423+
it('requires to pass all fields if nonOptionalFields is true', async () => {
424+
const TypeFactory = defineNonOptionalFields_OptionalFieldsTypeFactory({
425+
defaultFields: {
426+
field1: 'field1',
427+
// field2: 'field2',
428+
},
429+
});
430+
// field2 is not included if it is not passed to `defaultFields` or `build`.
431+
const type1 = await TypeFactory.build();
432+
expect(type1).toStrictEqual({ field1: 'field1' });
433+
expectTypeOf(type1).toEqualTypeOf<{ field1: string }>();
434+
435+
// field2 is included if it is passed to `defaultFields` or `build`.
436+
const type2 = await TypeFactory.build({ field2: 'field2' });
437+
expect(type2).toStrictEqual({ field1: 'field1', field2: 'field2' });
438+
expectTypeOf(type2).toEqualTypeOf<{ field1: string; field2: string }>();
439+
});
440+
});
411441
});
412442
describe('traits', () => {
413443
it('overrides defaultFields', async () => {

src/code-generator.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ function generateFieldNamesDefinitionCode(typeInfo: TypeInfo): string {
4040
return `const ${name}FieldNames = [${joinedFieldNames}] as const;\n`;
4141
}
4242

43-
function generateTypeFactoryCode(typeInfo: TypeInfo): string {
43+
function generateTypeFactoryCode(config: Config, typeInfo: TypeInfo): string {
4444
const { name } = typeInfo;
45+
function wrapRequired(str: string) {
46+
return config.nonOptionalFields ? `Required<${str}>` : str;
47+
}
4548
return `
4649
export type ${name}FactoryDefineOptions<
4750
TransientFields extends Record<string, unknown>,
@@ -57,7 +60,7 @@ export type ${name}FactoryInterface<
5760
5861
export function define${name}FactoryInternal<
5962
TransientFields extends Record<string, unknown>,
60-
_DefaultFieldsResolver extends DefaultFieldsResolver<Optional${name} & TransientFields>,
63+
_DefaultFieldsResolver extends ${wrapRequired(`DefaultFieldsResolver<Optional${name} & TransientFields>`)},
6164
_Traits extends Traits<Optional${name}, TransientFields>,
6265
>(
6366
options: ${name}FactoryDefineOptions<TransientFields, _DefaultFieldsResolver, _Traits>,
@@ -72,7 +75,7 @@ export function define${name}FactoryInternal<
7275
* @returns factory {@link ${name}FactoryInterface}
7376
*/
7477
export function define${name}Factory<
75-
_DefaultFieldsResolver extends DefaultFieldsResolver<Optional${name}>,
78+
_DefaultFieldsResolver extends ${wrapRequired(`DefaultFieldsResolver<Optional${name}>`)},
7679
_Traits extends Traits<Optional${name}, {}>,
7780
>(
7881
options: ${name}FactoryDefineOptions<{}, _DefaultFieldsResolver, _Traits>,
@@ -90,7 +93,7 @@ export function generateCode(config: Config, typeInfos: TypeInfo[]): string {
9093
code += '\n';
9194
code += generateFieldNamesDefinitionCode(typeInfo);
9295
code += '\n';
93-
code += generateTypeFactoryCode(typeInfo);
96+
code += generateTypeFactoryCode(config, typeInfo);
9497
code += '\n';
9598
}
9699
return code;

src/config.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ describe('validateConfig', () => {
2626
'`options.skipIsAbstractType` must be a boolean',
2727
);
2828
});
29+
it('nonOptionalFields', () => {
30+
expect(() => validateConfig({ typesFile: './types', nonOptionalFields: oneOf([true, false]) })).not.toThrow();
31+
expect(() => validateConfig({ typesFile: './types' })).not.toThrow();
32+
expect(() => validateConfig({ typesFile: './types', nonOptionalFields: 1 })).toThrow(
33+
'`options.nonOptionalFields` must be a boolean',
34+
);
35+
});
2936
it('typesPrefix', () => {
3037
expect(() => validateConfig({ typesFile: './types', typesPrefix: 'Prefix' })).not.toThrow();
3138
expect(() => validateConfig({ typesFile: './types' })).not.toThrow();

src/config.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type RawConfig = {
44
typesFile: string;
55
skipTypename?: RawTypesConfig['skipTypename'];
66
skipIsAbstractType?: boolean | undefined;
7+
nonOptionalFields?: boolean | undefined;
78
namingConvention?: RawTypesConfig['namingConvention'];
89
typesPrefix?: RawTypesConfig['typesPrefix'];
910
typesSuffix?: RawTypesConfig['typesSuffix'];
@@ -14,6 +15,7 @@ export type Config = {
1415
typesFile: string;
1516
skipTypename: Exclude<RawTypesConfig['skipTypename'], undefined>;
1617
skipIsAbstractType: boolean;
18+
nonOptionalFields: boolean;
1719
typesPrefix: Exclude<RawTypesConfig['typesPrefix'], undefined>;
1820
typesSuffix: Exclude<RawTypesConfig['typesSuffix'], undefined>;
1921
convert: ConvertFn;
@@ -36,6 +38,9 @@ export function validateConfig(rawConfig: unknown): asserts rawConfig is RawConf
3638
if ('skipIsAbstractType' in rawConfig && typeof rawConfig['skipIsAbstractType'] !== 'boolean') {
3739
throw new Error('`options.skipIsAbstractType` must be a boolean');
3840
}
41+
if ('nonOptionalFields' in rawConfig && typeof rawConfig['nonOptionalFields'] !== 'boolean') {
42+
throw new Error('`options.nonOptionalFields` must be a boolean');
43+
}
3944
if ('typesPrefix' in rawConfig && typeof rawConfig['typesPrefix'] !== 'string') {
4045
throw new Error('`options.typesPrefix` must be a string');
4146
}
@@ -49,9 +54,11 @@ export function normalizeConfig(rawConfig: RawConfig): Config {
4954
typesFile: rawConfig.typesFile,
5055
skipTypename: rawConfig.skipTypename ?? false,
5156
skipIsAbstractType: rawConfig.skipIsAbstractType ?? true,
57+
nonOptionalFields: rawConfig.nonOptionalFields ?? false,
5258
typesPrefix: rawConfig.typesPrefix ?? '',
5359
typesSuffix: rawConfig.typesSuffix ?? '',
54-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
55-
convert: convertFactory(rawConfig as any),
60+
convert: rawConfig.namingConvention
61+
? convertFactory({ namingConvention: rawConfig.namingConvention })
62+
: convertFactory({}),
5663
};
5764
}

src/helper/factory.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
FieldResolver,
99
} from './field-resolver.js';
1010
import { getSequenceCounter, resetSequence } from './sequence.js';
11-
import { Merge } from './util.js';
11+
import { Merge, StrictlyPick } from './util.js';
1212

1313
export type Traits<OptionalType extends Record<string, unknown>, TransientFields extends Record<string, unknown>> = {
1414
[traitName: string]: {
@@ -30,20 +30,22 @@ export interface TypeFactoryInterface<
3030
OptionalType extends Record<string, unknown>,
3131
TransientFields extends Record<string, unknown>,
3232
// NOTE: The constraints of _DefaultFieldsResolver are loose so that `Merge<_DefaultFieldsResolver, _Traits[T]['defaultFields']>` is accepted.
33-
_DefaultFieldsResolver extends Record<keyof OptionalType, FieldResolver<OptionalType & TransientFields, unknown>>,
33+
_DefaultFieldsResolver extends Partial<
34+
Record<keyof OptionalType, FieldResolver<OptionalType & TransientFields, unknown>>
35+
>,
3436
_Traits extends Traits<OptionalType, TransientFields>,
3537
> {
36-
build(): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<{}>>, keyof OptionalType>>;
38+
build(): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<{}>>, keyof OptionalType>>;
3739
build<T extends InputFieldsResolver<OptionalType & TransientFields>>(
3840
inputFieldsResolver: T,
39-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>>;
41+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>>;
4042
buildList(
4143
count: number,
42-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<{}>>, keyof OptionalType>[]>;
44+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<{}>>, keyof OptionalType>[]>;
4345
buildList<T extends InputFieldsResolver<OptionalType & TransientFields>>(
4446
count: number,
4547
inputFieldsResolver: T,
46-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[]>;
48+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[]>;
4749
use<T extends keyof _Traits>(
4850
traitName: T,
4951
): TypeFactoryInterface<
@@ -72,7 +74,7 @@ export function defineTypeFactoryInternal<
7274
return {
7375
async build<T extends InputFieldsResolver<OptionalType & TransientFields>>(
7476
inputFieldsResolver?: T,
75-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>> {
77+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>> {
7678
const seq = getSeq();
7779
return resolveFields<OptionalType, TransientFields, _DefaultFieldsResolver, T>(
7880
typeFieldNames,
@@ -84,8 +86,11 @@ export function defineTypeFactoryInternal<
8486
async buildList<T extends InputFieldsResolver<OptionalType & TransientFields>>(
8587
count: number,
8688
inputFieldsResolver?: T,
87-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[]> {
88-
const array: Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[] = [];
89+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[]> {
90+
const array: StrictlyPick<
91+
Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>,
92+
keyof OptionalType
93+
>[] = [];
8994
for (let i = 0; i < count; i++) {
9095
if (inputFieldsResolver) {
9196
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-explicit-any

src/helper/field-resolver.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ it('DefaultFieldsResolver', () => {
5757
type OptionalTypeWithTransientFields = { a: number | undefined; b: OptionalSubType[] | undefined };
5858
type OptionalSubType = { c: number | undefined };
5959
expectTypeOf<DefaultFieldsResolver<OptionalTypeWithTransientFields>>().toEqualTypeOf<{
60-
a: number | undefined | Dynamic<OptionalTypeWithTransientFields, number | undefined>;
61-
b:
60+
a?: number | undefined | Dynamic<OptionalTypeWithTransientFields, number | undefined>;
61+
b?:
6262
| readonly { readonly c: number | undefined }[]
6363
| undefined
6464
| Dynamic<OptionalTypeWithTransientFields, readonly { readonly c: number | undefined }[] | undefined>;

src/helper/field-resolver.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { DeepReadonly, Merge } from './util.js';
1+
import { DeepReadonly, Merge, StrictlyPick } from './util.js';
22

33
export type FieldResolverOptions<OptionalTypeWithTransientFields> = {
44
seq: number;
55
get: <FieldName extends keyof OptionalTypeWithTransientFields>(
66
fieldName: FieldName,
7-
) => Promise<DeepReadonly<OptionalTypeWithTransientFields[FieldName]>>;
7+
) => Promise<DeepReadonly<OptionalTypeWithTransientFields[FieldName]> | undefined>;
88
};
99

1010
export class Dynamic<OptionalTypeWithTransientFields, Field> {
@@ -29,15 +29,14 @@ export type FieldResolver<OptionalTypeWithTransientFields, Field> =
2929
| Dynamic<OptionalTypeWithTransientFields, Field>;
3030
/** The type of `defaultFields` option of `defineFactory` function. */
3131
export type DefaultFieldsResolver<OptionalTypeWithTransientFields> = {
32-
[FieldName in keyof OptionalTypeWithTransientFields]: FieldResolver<
32+
[FieldName in keyof OptionalTypeWithTransientFields]?: FieldResolver<
3333
OptionalTypeWithTransientFields,
3434
DeepReadonly<OptionalTypeWithTransientFields[FieldName]>
3535
>;
3636
};
3737
/** The type of `inputFields` option of `build` method. */
38-
export type InputFieldsResolver<OptionalTypeWithTransientFields> = Partial<
39-
DefaultFieldsResolver<OptionalTypeWithTransientFields>
40-
>;
38+
export type InputFieldsResolver<OptionalTypeWithTransientFields> =
39+
DefaultFieldsResolver<OptionalTypeWithTransientFields>;
4140

4241
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4342
export type ResolvedField<T extends FieldResolver<unknown, unknown>> = T extends FieldResolver<infer _, infer R>
@@ -59,7 +58,7 @@ export async function resolveFields<
5958
defaultFieldsResolver: _DefaultFieldsResolver,
6059
inputFieldsResolver: _InputFieldsResolver,
6160
): Promise<
62-
Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<_InputFieldsResolver>>, keyof OptionalType>
61+
StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<_InputFieldsResolver>>, keyof OptionalType>
6362
> {
6463
type OptionalTypeWithTransientFields = OptionalType & TransientFields;
6564

@@ -79,13 +78,17 @@ export async function resolveFields<
7978

8079
async function resolveFieldAndUpdateCache<FieldName extends keyof OptionalTypeWithTransientFields>(
8180
fieldName: FieldName,
82-
): Promise<DeepReadonly<OptionalTypeWithTransientFields[FieldName]>> {
81+
): Promise<DeepReadonly<OptionalTypeWithTransientFields[FieldName]> | undefined> {
8382
if (fieldName in fields) return fields[fieldName];
8483

85-
const fieldResolver =
86-
fieldName in inputFieldsResolver
87-
? inputFieldsResolver[fieldName as keyof _InputFieldsResolver]
88-
: defaultFieldsResolver[fieldName as keyof _DefaultFieldsResolver];
84+
let fieldResolver: FieldResolver<OptionalType & TransientFields, unknown>;
85+
if (fieldName in inputFieldsResolver) {
86+
fieldResolver = inputFieldsResolver[fieldName as keyof _InputFieldsResolver];
87+
} else if (fieldName in defaultFieldsResolver) {
88+
fieldResolver = defaultFieldsResolver[fieldName as keyof _DefaultFieldsResolver];
89+
} else {
90+
return undefined;
91+
}
8992

9093
// eslint-disable-next-line require-atomic-updates
9194
fields[fieldName] = await resolveField(options, fieldResolver);
@@ -97,7 +100,7 @@ export async function resolveFields<
97100
get: resolveFieldAndUpdateCache,
98101
};
99102

100-
for (const fieldName of Object.keys(defaultFieldsResolver) as (keyof OptionalType)[]) {
103+
for (const fieldName of fieldNames) {
101104
// eslint-disable-next-line no-await-in-loop
102105
await resolveFieldAndUpdateCache(fieldName);
103106
}

0 commit comments

Comments
 (0)