import type { SetTypeSubArg } from '@aws-amplify/data-schema-types'; import { type PrimaryIndexIrShape, type SecondaryIndexIrShape, brand, } from './util'; import type { InternalField, BaseModelField } from './ModelField'; import type { ModelRelationshipField, InternalRelationshipField, ModelRelationshipFieldParamShape, } from './ModelRelationshipField'; import { type Authorization, type BaseAllowModifier, type AnyAuthorization, allow, } from './Authorization'; import type { RefType, RefTypeParamShape } from './RefType'; import type { EnumType } from './EnumType'; import type { CustomType, CustomTypeParamShape } from './CustomType'; import { type ModelIndexType, type InternalModelIndexType, modelIndex, } from './ModelIndex'; import type { PrimaryIndexFieldsToIR, SecondaryIndexToIR, } from './MappedTypes/MapIndexes'; import type { brandSymbol } from './util/Brand.js'; import type { methodKeyOf } from './util/usedMethods.js'; const brandName = 'modelType'; export type deferredRefResolvingPrefix = 'deferredRefResolving:'; type ModelFields = Record< string, | BaseModelField | ModelRelationshipField | RefType | EnumType | CustomType >; type InternalModelFields = Record< string, InternalField | InternalRelationshipField >; export type DisableOperationsOptions = | 'queries' | 'mutations' | 'subscriptions' | 'list' | 'get' | 'create' | 'update' | 'delete' | 'onCreate' | 'onUpdate' | 'onDelete'; type ModelData = { fields: ModelFields; identifier: ReadonlyArray; secondaryIndexes: ReadonlyArray>; authorization: Authorization[]; disabledOperations: ReadonlyArray; }; type InternalModelData = ModelData & { fields: InternalModelFields; identifier: ReadonlyArray; secondaryIndexes: ReadonlyArray; authorization: Authorization[]; disabledOperations: ReadonlyArray; originalName?: string; }; export type ModelTypeParamShape = { fields: ModelFields; identifier: PrimaryIndexIrShape; secondaryIndexes: ReadonlyArray; authorization: Authorization[]; disabledOperations: ReadonlyArray; }; /** * Extract fields that are eligible to be PK or SK fields with their resolved type. * * Eligible fields include: * 1. ModelField that contains string or number * 2. inline EnumType * 3. RefType that refers to a top level defined EnumType (this is enforced by * validation that happens in the Schema Processor) * * NOTE: at this point, there is no way to resolve the type from a RefType as * we don't have access to the NonModelType at this location. So we generate am * indicator string, and resolve its corresponding type later in * packages/data-schema/src/runtime/client/index.ts */ export type ExtractSecondaryIndexIRFields< T extends ModelTypeParamShape, > = { [FieldProp in keyof T['fields'] as T['fields'][FieldProp] extends BaseModelField< infer R > ? NonNullable extends string | number ? FieldProp : never : T['fields'][FieldProp] extends | EnumType | RefType ? FieldProp : never]: T['fields'][FieldProp] extends BaseModelField ? R : T['fields'][FieldProp] extends EnumType ? values[number] : T['fields'][FieldProp] extends RefType ? `${deferredRefResolvingPrefix}${R['link']}` : never; }; type ExtractType = { [FieldProp in keyof T['fields'] as T['fields'][FieldProp] extends BaseModelField ? FieldProp : never]: T['fields'][FieldProp] extends BaseModelField ? R : never; }; /** * For a given ModelTypeParamShape, produces a map of Authorization rules * that would *conflict* with the given type. * * E.g., * * ``` * const test = { * fields: { * title: fields.string(), * otherfield: fields.string().array(), * numfield: fields.integer(), * }, * identifier: [], * authorization: [], * }; * * ConflictingAuthRulesMap === { * title: Authorization<"title", true>; * otherfield: Authorization<"otherfield", false>; * numfield: Authorization<"numfield", true> | Authorization<"numfield", false>; * } * ``` */ type ConflictingAuthRulesMap = { [K in keyof ExtractType]: K extends string ? string extends ExtractType[K] ? Authorization : string[] extends ExtractType[K] ? Authorization : Authorization | Authorization : never; }; export type AddRelationshipFieldsToModelTypeFields< Model, RelationshipFields extends Record< string, ModelRelationshipField >, > = Model extends ModelType< infer ModelParam extends ModelTypeParamShape, infer HiddenKeys > ? ModelType< SetTypeSubArg< ModelParam, 'fields', ModelParam['fields'] & RelationshipFields >, HiddenKeys > : never; /** * For a given ModelTypeParamShape, produces a union of Authorization rules * that would *conflict* with the given type. * * E.g., * * ``` * const test = { * fields: { * title: fields.string(), * otherfield: fields.string().array(), * numfield: fields.integer(), * }, * identifier: [], * authorization: [], * }; * * ConflictingAuthRules === * Authorization<"title", true> * | Authorization<"otherfield", false> * | Authorization<"numfield", true> | Authorization<"numfield", false> * ; * ``` */ type _ConflictingAuthRules = ConflictingAuthRulesMap[keyof ConflictingAuthRulesMap]; export type BaseModelType = ModelType; export type UsableModelTypeKey = methodKeyOf; /** * Model type definition interface * * @param T - The shape of the model type * @param UsedMethod - The method keys already defined */ export type ModelType< T extends ModelTypeParamShape = ModelTypeParamShape, UsedMethod extends UsableModelTypeKey = never, > = Omit< { [brandSymbol]: typeof brandName; /** * Defines single-field or composite identifiers, the fields must be marked required * * @param identifier A list of field names used as identifiers for the data model * @returns A ModelType instance with updated identifiers * * @example * a.model({ * name: a.string().required(), * email: a.string().required(), * age: a.integer(), * }).identifier(['name', 'email']) */ identifier< PrimaryIndexFields = ExtractSecondaryIndexIRFields, PrimaryIndexPool extends string = keyof PrimaryIndexFields & string, const ID extends ReadonlyArray = readonly [], const PrimaryIndexIR extends PrimaryIndexIrShape = PrimaryIndexFieldsToIR< ID, PrimaryIndexFields >, >( identifier: ID, ): ModelType< SetTypeSubArg, 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 A ModelType instance with updated secondary index * * @example * a.model().secondaryIndexes((index) => [index('type').sortKeys(['sort'])]) * * @see [Amplify documentation for secondary indexes](https://docs.amplify.aws/react/build-a-backend/data/data-modeling/secondary-index/) * @see [Amazon DynamoDB documentation for secondary indexes](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SecondaryIndexes.html) */ secondaryIndexes< const SecondaryIndexFields = ExtractSecondaryIndexIRFields, 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: PK, ) => ModelIndexType< SecondaryIndexPKPool, PK, ReadonlyArray> >, ) => Indexes, ): ModelType< SetTypeSubArg, UsedMethod | 'secondaryIndexes' >; /** * Disables the specified operations for the model * * @param ops A list of operations to be disabled * @returns A ModelType instance with updated disabled operations * * @example * a.model().disableOperations(['delete', 'update', 'queries', 'subscriptions']) * * @see [Amplify Data documentation for supported operations](https://docs.amplify.aws/react/build-a-backend/data/) */ disableOperations< const Ops extends ReadonlyArray, >( ops: Ops, ): ModelType< SetTypeSubArg, UsedMethod | 'disableOperations' >; /** * 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 A ModelType instance with updated authorization rules * * @example * a.model().authorization((allow) => [ * allow.guest(), * allow.publicApiKey(), * allow.authenticated(), * ]) */ authorization( callback: ( allow: BaseAllowModifier, ) => AuthRuleType | AuthRuleType[], ): ModelType< SetTypeSubArg, UsedMethod | 'authorization' >; }, UsedMethod >; /** * External representation of Model Type that exposes the `relationships` modifier. * Used on the complete schema object. */ export type SchemaModelType< T extends BaseModelType = ModelType, ModelName extends string = string, IsRDS extends boolean = false, > = IsRDS extends true ? T & { relationships< Param extends Record< string, ModelRelationshipField > = Record, >( relationships: Param, ): Record; fields: T extends ModelType ? R['fields'] : never; } : T; /** * Internal representation of Model Type that exposes the `data` property. * Used at buildtime. */ export type InternalModel = SchemaModelType< ModelType, string, true > & { data: InternalModelData; }; function _model(fields: T['fields']) { const data: ModelData = { fields, identifier: ['id'], secondaryIndexes: [], authorization: [], disabledOperations: [], }; const builder = { identifier(identifier) { data.identifier = identifier; return this; }, secondaryIndexes(callback) { data.secondaryIndexes = callback(modelIndex); return this; }, disableOperations(ops) { data.disabledOperations = ops; return this; }, authorization(callback) { const { resource: _, ...rest } = allow; const rules = callback(rest); data.authorization = Array.isArray(rules) ? rules : [rules]; return this; }, ...brand(brandName), } as ModelType; return { ...builder, data, relationships(relationships) { data.fields = { ...data.fields, ...relationships }; }, fields: data.fields, } as InternalModel as ModelType; } /** * Model Type type guard * @param modelType - api-next ModelType * @returns true if the given value is a ModelSchema */ export const isSchemaModelType = ( modelType: any | SchemaModelType, ): modelType is SchemaModelType => { const internalType = modelType as InternalModel; return ( typeof internalType === 'object' && internalType.data !== undefined && internalType.data.fields !== undefined && internalType.data.authorization !== undefined && internalType.data.identifier !== undefined && internalType.data.secondaryIndexes !== undefined && typeof internalType.relationships === 'function' ); }; /** * Model default identifier * * @param pk - primary key * @param sk - secondary key * @param compositeSk - composite secondary key */ export type ModelDefaultIdentifier = { pk: { readonly id: string }; sk: never; compositeSk: never; }; /** * A data model that creates a matching Amazon DynamoDB table and provides create, read (list and get), update, * delete, and subscription APIs. * * @param fields database table fields. Supports scalar types and relationship types. * @returns a data model definition */ export function model( fields: T, ): ModelType<{ fields: T; identifier: ModelDefaultIdentifier; secondaryIndexes: []; authorization: []; disabledOperations: []; }> { return _model(fields); }