Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/large-squids-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/data-schema': patch
---

fix: custom sel. set return type for array custom types
5 changes: 5 additions & 0 deletions .changeset/selfish-comics-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/data-schema': minor
---

Disable additional .array() modifier on model field definition
5 changes: 5 additions & 0 deletions packages/data-schema/__tests__/ModelField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,8 @@ describe('field level auth', () => {
expect(field.data.authorization).toMatchSnapshot();
});
});

it('array modifier becomes unavailable after being used once', () => {
// @ts-expect-error .array() is not a valid modifier after being used once
a.model({ values: a.string().required().array().required().array().required() });
});
3 changes: 2 additions & 1 deletion packages/data-schema/docs/data-schema.modelfield.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ Public API for the chainable builder methods exposed by Model Field. The type is
export type ModelField<T extends ModelFieldTypeParamOuter = ModelFieldTypeParamOuter, UsedMethod extends UsableModelFieldKey = never, Auth = undefined> = Omit<{
[__auth]?: Auth;
[brandSymbol]: typeof brandName;
[internal](): ModelField<T>;
required(): ModelField<Required<T>, UsedMethod | 'required'>;
array(): ModelField<ArrayField<T>, Exclude<UsedMethod, 'required'>>;
array(): ModelField<ArrayField<T>, Exclude<UsedMethod, 'required'> | 'array'>;
default(value?: ModelFieldTypeParamOuter): ModelField<T, UsedMethod | 'default'>;
authorization<AuthRuleType extends Authorization<any, any, any>>(callback: (allow: Omit<AllowModifier, 'resource'>) => AuthRuleType | AuthRuleType[]): ModelField<T, UsedMethod | 'authorization', AuthRuleType>;
}, UsedMethod>;
Expand Down
16 changes: 14 additions & 2 deletions packages/data-schema/src/ModelField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const __auth = Symbol('__auth');
export const __generated = Symbol('__generated');

const brandName = 'modelField';
const internal = Symbol('internal');

export enum ModelFieldType {
Id = 'ID',
Expand Down Expand Up @@ -82,7 +83,7 @@ export type BaseModelField<

export type UsableModelFieldKey = satisfy<
methodKeyOf<ModelField>,
'required' | 'default' | 'authorization'
'required' | 'default' | 'authorization' | 'array'
>;

/**
Expand All @@ -102,6 +103,14 @@ export type ModelField<
[__auth]?: Auth;
[brandSymbol]: typeof brandName;

/**
* Internal non-omittable method that allows `BaseModelField` to retain a reference to `T` type arg in `ModelField`.
* Since all public methods are omittable, the evaluated `BaseModelField` loses type information unless
* some property on the type is guaranteed to reference `T`
* Context: https://github.com/aws-amplify/amplify-api-next/pull/406/files#r1869481467
*/
[internal](): ModelField<T>;

/**
* Marks a field as required.
*/
Expand All @@ -110,7 +119,7 @@ export type ModelField<
/**
* Converts a field type definition to an array of the field type.
*/
array(): ModelField<ArrayField<T>, Exclude<UsedMethod, 'required'>>;
array(): ModelField<ArrayField<T>, Exclude<UsedMethod, 'required'> | 'array'>;
// TODO: should be T, but .array breaks this constraint. Fix later
/**
* Sets a default value for the scalar type.
Expand Down Expand Up @@ -199,6 +208,9 @@ function _field<T extends ModelFieldTypeParamOuter>(fieldType: ModelFieldType) {
return this;
},
...brand(brandName),
[internal]() {
return this;
},
} as ModelField<T>;

// this double cast gives us a Subtyping Constraint i.e., hides `data` from the public API,
Expand Down
49 changes: 40 additions & 9 deletions packages/data-schema/src/runtime/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,52 @@ type ReturnValue<
* This mapped type traverses the SelectionSetReturnValue result and the original FlatModel, restoring array types
* that were flattened in DeepPickFromPath
*
* Note: custom type field arrays are already handled correctly and don't need to be "restored", hence the `Result[K] extends Array<any>` check
* @typeParam Result - this is the result of applying the selection set path to FlatModel; return type of UnionToIntersection<DeepPickFromPath<FlatModel, Paths>>
* @typeParam FlatModel - the reference model shape; return type of ResolvedModel<Model>
*
* Note: we wrap `Result` and `FlatModel` in NonNullable, because recursive invocations of this mapped type
* can result in the type arguments containing `{} | null | undefined` which breaks indexed access, e.g. Result[K]
*
* Using NonNullable<> directly inside the mapped type is significantly more performant here than attempting to pre-compute in the type params,
* e.g., `type RestoreArrays<Result, FlatModel, NonNullableResult = NonNullable<Result>, NonNullableFlatModel = NonNullable<FlatModel>> = {...}`
*/
type RestoreArrays<Result, FlatModel> = {
[K in keyof Result]: K extends keyof FlatModel
? FlatModel[K] extends Array<any>
? Result[K] extends Array<any>
? Result[K]
: Array<RestoreArrays<Result[K], UnwrapArray<FlatModel[K]>>>
: FlatModel[K] extends Record<string, any>
? RestoreArrays<Result[K], FlatModel[K]>
: Result[K]
[K in keyof NonNullable<Result>]: K extends keyof NonNullable<FlatModel>
? Array<any> extends NonNullable<FlatModel>[K]
? HandleArrayNullability<
NonNullable<Result>[K],
NonNullable<FlatModel>[K]
>
: NonNullable<FlatModel>[K] extends Record<string, any>
? RestoreArrays<NonNullable<Result>[K], NonNullable<FlatModel>[K]>
: NonNullable<Result>[K]
: never;
};

/**
* This mapped type gets called by RestoreArrays<T, K> and it restores the expected
* nullability in array fields (e.g. nullable vs. required value & nullable vs. required array)
*/
type HandleArrayNullability<Result, FlatModel> =
Array<any> extends Result
? // If Result is already an array, return it as is.
Result
: NonNullable<FlatModel> extends Array<infer InnerValue>
? // is the array nullable?
null extends FlatModel
? // is the value nullable?
null extends InnerValue
? // value and array are nullable - a.ref('SomeType').array()
Array<RestoreArrays<Result, UnwrapArray<FlatModel>> | null> | null
: // value required; array nullable - a.ref('SomeType').required().array()
Array<RestoreArrays<Result, UnwrapArray<FlatModel>>> | null
: null extends InnerValue
? // value nullable; array required - a.ref('SomeType').array().required()
Array<RestoreArrays<Result, UnwrapArray<FlatModel>> | null>
: // value required; array required - a.ref('SomeType').required().array().required()
Array<RestoreArrays<Result, UnwrapArray<FlatModel>>>
: never;

/**
* Generates flattened, readonly return type using specified custom sel. set
*/
Expand Down
Loading
Loading