import type { DerivedApiDefinition, SetTypeSubArg, SchemaConfiguration, DataSourceConfiguration, DatasourceEngine, UnionToIntersection, } from '@aws-amplify/data-schema-types'; import { type InternalModel, isSchemaModelType, SchemaModelType, AddRelationshipFieldsToModelTypeFields, type BaseModelType, } from './ModelType'; import type { EnumType } from './EnumType'; import type { CustomType, CustomTypeParamShape } from './CustomType'; import type { CustomOperation, CustomOperationParamShape, InternalCustom, MutationCustomOperation, QueryCustomOperation, SubscriptionCustomOperation, } from './CustomOperation'; import { processSchema } from './SchemaProcessor'; import { AllowModifier, SchemaAuthorization, allow } from './Authorization'; import { Brand, brand, getBrand, RenameUsingTuples } from './util'; import { ModelRelationshipField, ModelRelationshipFieldParamShape, } from './ModelRelationshipField'; import { ConversationType } from './ai/ConversationType'; export { ModelType } from './ModelType'; export { EnumType } from './EnumType'; export { CustomType } from './CustomType'; export { CustomOperation } from './CustomOperation'; export { ConversationType } from './ai/ConversationType'; export const rdsSchemaBrandName = 'RDSSchema'; export const rdsSchemaBrand = brand(rdsSchemaBrandName); export type RDSSchemaBrand = Brand; export const ddbSchemaBrandName = 'DDBSchema'; const ddbSchemaBrand = brand(ddbSchemaBrandName); export type DDBSchemaBrand = Brand; type SchemaContent = | BaseModelType | CustomType | EnumType | CustomOperation | ConversationType; // The SQL-only `addToSchema` accepts all top-level entities, excepts models type AddToSchemaContent = Exclude; type AddToSchemaContents = Record; type NonEmpty = keyof T extends never ? never : T; export type ModelSchemaContents = Record; type InternalSchemaModels = Record< string, InternalModel | EnumType | CustomType | InternalCustom >; export type ModelSchemaParamShape = { types: ModelSchemaContents; authorization: SchemaAuthorization[]; configuration: SchemaConfiguration; }; export type RDSModelSchemaParamShape = ModelSchemaParamShape; export type InternalSchema = { data: { types: InternalSchemaModels; authorization: SchemaAuthorization[]; configuration: SchemaConfiguration; }; context?: { schemas: InternalSchema[]; }; }; export type BaseSchema< T extends ModelSchemaParamShape, IsRDS extends boolean = false, > = { data: T; models: { [TypeKey in keyof T['types']]: T['types'][TypeKey] extends BaseModelType ? SchemaModelType : never; }; transform: () => DerivedApiDefinition; context?: { schemas: GenericModelSchema[]; }; }; export type GenericModelSchema = BaseSchema & Brand; /** * Model schema definition interface * * @param T - The shape of the model schema * @param UsedMethods - The method keys already defined */ export type ModelSchema< T extends ModelSchemaParamShape, UsedMethods extends 'authorization' | 'relationships' = never, > = Omit< { authorization: >( callback: (allow: AllowModifier) => AuthRules | AuthRules[], ) => ModelSchema< SetTypeSubArg, UsedMethods | 'authorization' >; }, UsedMethods > & BaseSchema & DDBSchemaBrand; type RDSModelSchemaFunctions = | 'addToSchema' | 'addQueries' | 'addMutations' | 'addSubscriptions' | 'authorization' | 'setRelationships' | 'setAuthorization' | 'renameModelFields' | 'renameModels'; type OmitFromEach = { [ModelName in keyof Models]: Omit; }; type RelationshipTemplate = Record< string, ModelRelationshipField >; /** * RDSModel schema definition interface * * @param T - The shape of the RDS model schema * @param UsedMethods - The method keys already defined */ export type RDSModelSchema< T extends RDSModelSchemaParamShape, UsedMethods extends RDSModelSchemaFunctions = never, > = Omit< { addToSchema: ( types: AddedTypes, ) => RDSModelSchema< SetTypeSubArg, UsedMethods | 'addToSchema' >; /** * @deprecated use `addToSchema()` to add operations to a SQL schema */ addQueries: >( types: Queries, ) => RDSModelSchema< SetTypeSubArg, UsedMethods | 'addQueries' >; /** * @deprecated use `addToSchema()` to add operations to a SQL schema */ addMutations: >( types: Mutations, ) => RDSModelSchema< SetTypeSubArg, UsedMethods | 'addMutations' >; /** * @deprecated use `addToSchema()` to add operations to a SQL schema */ addSubscriptions: < Subscriptions extends Record, >( types: Subscriptions, ) => RDSModelSchema< SetTypeSubArg, UsedMethods | 'addSubscriptions' >; // TODO: hide this, since SQL schema auth is configured via .setAuthorization? authorization: >( callback: (allow: AllowModifier) => AuthRules | AuthRules[], ) => RDSModelSchema< SetTypeSubArg, UsedMethods | 'authorization' >; setAuthorization: ( callback: ( models: OmitFromEach['models'], 'secondaryIndexes'>, schema: RDSModelSchema, ) => void, ) => RDSModelSchema; setRelationships: < Relationships extends ReadonlyArray< Partial> >, >( callback: ( models: OmitFromEach< BaseSchema['models'], 'authorization' | 'fields' | 'secondaryIndexes' >, ) => Relationships, ) => RDSModelSchema< SetTypeSubArg< T, 'types', { [ModelName in keyof T['types']]: ModelWithRelationships< T['types'], Relationships, ModelName >; } >, UsedMethods | 'setRelationships' >; renameModels: < NewName extends string, CurName extends string = keyof BaseSchema['models'] & string, const ChangeLog extends readonly [CurName, NewName][] = [], >( callback: () => ChangeLog, ) => RDSModelSchema< SetTypeSubArg>, UsedMethods | 'renameModels' >; }, UsedMethods > & BaseSchema & RDSSchemaBrand; /** * Amplify API Next Model Schema shape */ export type ModelSchemaType = ModelSchema; type ModelWithRelationships< Types extends Record, Relationships extends ReadonlyArray< Record >, ModelName extends keyof Types, RelationshipMap extends UnionToIntersection< Relationships[number] > = UnionToIntersection, > = ModelName extends keyof RelationshipMap ? RelationshipMap[ModelName] extends Record< string, ModelRelationshipField > ? AddRelationshipFieldsToModelTypeFields< Types[ModelName], RelationshipMap[ModelName] > : Types[ModelName] : Types[ModelName]; /** * Filter the schema types down to only include the ModelTypes as SchemaModelType * * @param schemaContents The object containing all SchemaContent for this schema * @returns Only the schemaContents that are ModelTypes, coerced to the SchemaModelType surface */ const filterSchemaModelTypes = ( schemaContents: ModelSchemaContents, ): Record => { const modelTypes: Record = {}; if (schemaContents) { Object.entries(schemaContents).forEach(([key, content]) => { if (isSchemaModelType(content)) { modelTypes[key] = content; } }); } return modelTypes; }; /** * Model Schema type guard * @param schema - api-next ModelSchema or string * @returns true if the given value is a ModelSchema */ export const isModelSchema = ( schema: string | ModelSchemaType, ): schema is ModelSchemaType => { return typeof schema === 'object' && schema.data !== undefined; }; /** * Ensures that only supported entities are being added to the SQL schema through `addToSchema` * Models are not supported for brownfield SQL * * @param types - purposely widened to ModelSchemaContents, because we need to validate at runtime that a model is not being passed in here */ function validateAddToSchema(types: ModelSchemaContents): void { for (const [name, type] of Object.entries(types)) { if (getBrand(type) === 'modelType') { throw new Error( `Invalid value specified for ${name} in addToSchema(). Models cannot be manually added to a SQL schema.`, ); } } } function _rdsSchema< T extends RDSModelSchemaParamShape, DSC extends SchemaConfiguration, >(types: T['types'], config: DSC): RDSModelSchema { const data: RDSModelSchemaParamShape = { types, authorization: [], configuration: config, }; const models = filterSchemaModelTypes(data.types) as any; return { data, models, transform(): DerivedApiDefinition { const internalSchema: InternalSchema = { data, context: this.context, } as InternalSchema; return processSchema({ schema: internalSchema }); }, authorization(callback): any { const rules = callback(allow); this.data.authorization = Array.isArray(rules) ? rules : [rules]; const { authorization: _, ...rest } = this; return rest; }, addToSchema(types: AddToSchemaContents): any { validateAddToSchema(types); this.data.types = { ...this.data.types, ...types }; const { addToSchema: _, ...rest } = this; return rest; }, addQueries(types: Record): any { this.data.types = { ...this.data.types, ...types }; const { addQueries: _, ...rest } = this; return rest; }, addMutations(types: Record): any { this.data.types = { ...this.data.types, ...types }; const { addMutations: _, ...rest } = this; return rest; }, addSubscriptions(types: Record): any { this.data.types = { ...this.data.types, ...types }; const { addSubscriptions: _, ...rest } = this; return rest; }, setAuthorization(callback) { callback(models, this); const { setAuthorization: _, ...rest } = this; return rest; }, setRelationships(callback): any { const { setRelationships: _, ...rest } = this; // The relationships are added via `models..relationships` // modifiers that's being called within the callback. They are modifying // by references on each model, so there is not anything else to be done // here. callback(models); return rest; }, renameModels(callback): any { const { renameModels: _, ...rest } = this; // returns an array of tuples [curName, newName] const changeLog = callback(); changeLog.forEach(([curName, newName]) => { const currentType = data.types[curName]; if (currentType === undefined) { throw new Error( `Invalid renameModels call. ${curName} is not defined in the schema`, ); } if (typeof newName !== 'string' || newName.length < 1) { throw new Error( `Invalid renameModels call. New name must be a non-empty string. Received: "${newName}"`, ); } models[newName] = currentType; data.types[newName] = currentType; models[newName].data.originalName = curName; delete models[curName]; delete data.types[curName]; }); return rest; }, ...rdsSchemaBrand, } as RDSModelSchema; } function _ddbSchema< T extends ModelSchemaParamShape, DSC extends SchemaConfiguration, >(types: T['types'], config: DSC): ModelSchema { const data: ModelSchemaParamShape = { types, authorization: [], configuration: config, }; return { data, transform(): DerivedApiDefinition { const internalSchema = { data, context: this.context, }; return processSchema({ schema: internalSchema }); }, authorization(callback): any { const rules = callback(allow); this.data.authorization = Array.isArray(rules) ? rules : [rules]; const { authorization: _, ...rest } = this; return rest; }, models: filterSchemaModelTypes(data.types), ...ddbSchemaBrand, } satisfies ModelSchema as never; } type SchemaReturnType< DE extends DatasourceEngine, Types extends ModelSchemaContents, > = DE extends 'dynamodb' ? ModelSchema<{ types: Types; authorization: []; configuration: any; }> : RDSModelSchema<{ types: Types; authorization: []; configuration: any; }>; function bindConfigToSchema( config: SchemaConfiguration>, ): ( types: NonEmpty, ) => SchemaReturnType { return (types) => { return ( config.database.engine === 'dynamodb' ? _ddbSchema(types, config) : _rdsSchema(types, config) ) as SchemaReturnType; }; } /** * The API and data model definition for Amplify Data. Pass in `{ : a.model(...) }` to create a database table * and exposes CRUDL operations via an API. * @param types The API and data model definition * @returns An API and data model definition to be deployed with Amplify (Gen 2) experience (`processSchema(...)`) * or with the Amplify Data CDK construct (`@aws-amplify/data-construct`) */ export const schema = bindConfigToSchema({ database: { engine: 'dynamodb' } }); /** * Configure wraps schema definition with non-default config to allow usecases other than * the default DynamoDB use-case. * * @param config The SchemaConfig augments the schema with content like the database type * @returns */ export function configure( config: SchemaConfiguration>, ): { schema: ( types: NonEmpty, ) => SchemaReturnType; } { return { schema: bindConfigToSchema(config), }; } export function isCustomPathData(obj: any): obj is CustomPathData { return ( 'stack' in obj && (typeof obj.stack === 'undefined' || typeof obj.stack === 'string') && 'entry' in obj && typeof obj.entry === 'string' ); } export type CustomPathData = { stack: string | undefined; entry: string; };