Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add docstring and flatten param types for schema builder methods #407

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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 .changeset/three-beers-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/data-schema': patch
---

Flatten param types for authorization builder
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
5 changes: 3 additions & 2 deletions packages/data-schema/docs/data-schema.modeltype.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export type ModelType<T extends ModelTypeParamShape = ModelTypeParamShape, UsedM
identifier<PrimaryIndexFields = ExtractSecondaryIndexIRFields<T>, PrimaryIndexPool extends string = keyof PrimaryIndexFields & string, const ID extends ReadonlyArray<PrimaryIndexPool> = readonly [], const PrimaryIndexIR extends PrimaryIndexIrShape = PrimaryIndexFieldsToIR<ID, PrimaryIndexFields>>(identifier: ID): ModelType<SetTypeSubArg<T, 'identifier', PrimaryIndexIR>, UsedMethod | 'identifier'>;
secondaryIndexes<const SecondaryIndexFields = ExtractSecondaryIndexIRFields<T>, const SecondaryIndexPKPool extends string = keyof SecondaryIndexFields & string, const Indexes extends readonly ModelIndexType<string, string, unknown, readonly [], any>[] = readonly [], const IndexesIR extends readonly any[] = SecondaryIndexToIR<Indexes, SecondaryIndexFields>>(callback: (index: <PK extends SecondaryIndexPKPool>(pk: PK) => ModelIndexType<SecondaryIndexPKPool, PK, ReadonlyArray<Exclude<SecondaryIndexPKPool, PK>>>) => Indexes): ModelType<SetTypeSubArg<T, 'secondaryIndexes', IndexesIR>, UsedMethod | 'secondaryIndexes'>;
disableOperations<const Ops extends ReadonlyArray<DisableOperationsOptions>>(ops: Ops): ModelType<SetTypeSubArg<T, 'disabledOperations', Ops>, UsedMethod | 'disableOperations'>;
authorization<AuthRuleType extends Authorization<any, any, any>>(callback: (allow: Omit<AllowModifier, 'resource'>) => AuthRuleType | AuthRuleType[]): ModelType<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, UsedMethod | 'authorization'>;
authorization<AuthRuleType extends AnyAuthorization>(callback:
(allow: BaseAllowModifier) => AuthRuleType | AuthRuleType[]): ModelType<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, UsedMethod | 'authorization'>;
}, UsedMethod>;
```
**References:** [ModelType](./data-schema.modeltype.md)<!-- -->, [Authorization](./data-schema.authorization.md)
**References:** [ModelType](./data-schema.modeltype.md)

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
61 changes: 54 additions & 7 deletions packages/data-schema/src/ModelType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import type { methodKeyOf } from './util/usedMethods.js';
const brandName = 'modelType';
export type deferredRefResolvingPrefix = 'deferredRefResolving:';

type BaseAllowModifier = Omit<AllowModifier, 'resource'>;
type AnyAuthorization = Authorization<any, any, any>;

type ModelFields = Record<
string,
| BaseModelField
Expand Down Expand Up @@ -95,9 +98,7 @@ export type ModelTypeParamShape = {
* indicator string, and resolve its corresponding type later in
* packages/data-schema/src/runtime/client/index.ts
*/
export type ExtractSecondaryIndexIRFields<
T extends ModelTypeParamShape,
> = {
export type ExtractSecondaryIndexIRFields<T extends ModelTypeParamShape> = {
[FieldProp in keyof T['fields'] as T['fields'][FieldProp] extends BaseModelField<
infer R
>
Expand Down Expand Up @@ -224,6 +225,16 @@ export type ModelType<
> = Omit<
{
[brandSymbol]: typeof brandName;

/**
* Defines single-field or composite identifiers
*
* @param identifier - A list of field names used as identifiers for the data model
* @returns {ModelType} A ModelType instance with updated identifiers
*
* @example
* .identifier(['name', 'age'])
*/
identifier<
PrimaryIndexFields = ExtractSecondaryIndexIRFields<T>,
PrimaryIndexPool extends string = keyof PrimaryIndexFields & string,
Expand All @@ -238,6 +249,16 @@ export type ModelType<
SetTypeSubArg<T, 'identifier', PrimaryIndexIR>,
UsedMethod | 'identifier'
>;

/**
* Adds secondary index for a model, secondary index consists of a "hash key" and optionally, a "sort key"
*
* @param callback - A function that specifies "hash key" and "sort key"
* @returns {ModelType} A ModelType instance with updated secondary index
*
* @example
* .secondaryIndexes((index) => [index('type').sortKeys(['sort'])])
*/
secondaryIndexes<
const SecondaryIndexFields = ExtractSecondaryIndexIRFields<T>,
const SecondaryIndexPKPool extends string = keyof SecondaryIndexFields &
Expand Down Expand Up @@ -267,6 +288,16 @@ export type ModelType<
SetTypeSubArg<T, 'secondaryIndexes', IndexesIR>,
UsedMethod | 'secondaryIndexes'
>;

/**
* Disables the specified operations for the model
*
* @param ops - A list of operations to be disabled
* @returns {ModelType} A ModelType instance with updated disabled operations
*
* @example
* model.disableOperations(['queries', 'subscriptions'])
*/
disableOperations<
const Ops extends ReadonlyArray<DisableOperationsOptions>,
>(
Expand All @@ -275,10 +306,26 @@ export type ModelType<
SetTypeSubArg<T, 'disabledOperations', Ops>,
UsedMethod | 'disableOperations'
>;
authorization<AuthRuleType extends Authorization<any, any, any>>(
callback: (
allow: Omit<AllowModifier, 'resource'>,
) => AuthRuleType | AuthRuleType[],

/**
* Configures authorization rules for public, signed-in user, per user, and per user group data access
*
* @param callback - A function that receives an allow modifier to define authorization rules
* @returns {ModelType} A ModelType instance with updated authorization rules
*
* @example
* model.authorization((allow) => [
* allow.guest(),
* allow.publicApiKey(),
* allow.authenticated(),
* ])
*/
authorization<AuthRuleType extends AnyAuthorization>(
callback:
/**
* Modifier that defines authorization rules.
*/
(allow: BaseAllowModifier) => AuthRuleType | AuthRuleType[],
): ModelType<
SetTypeSubArg<T, 'authorization', AuthRuleType[]>,
UsedMethod | 'authorization'
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