Skip to content

Commit

Permalink
Merge branch 'main' into feat/ai-conversation-update
Browse files Browse the repository at this point in the history
  • Loading branch information
atierian authored Oct 30, 2024
2 parents 990a8ed + 1506e90 commit aa4c942
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 82 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/data-schema/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @aws-amplify/data-schema

## 1.11.0

### Minor Changes

- 1a671ab: allow nullable identifiers

## 1.10.2

### Patch Changes
Expand Down
3 changes: 2 additions & 1 deletion packages/data-schema/__tests__/ModelType.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ describe('identifiers', () => {
title: string(),
});

// @ts-expect-error
// While allowed on schemas to facilitate SQL generated fields e.g. SERIAL,
// improperly assignging a nullable field as an identifier will result in build errors
m2.identifier(['title']);
});

Expand Down
33 changes: 29 additions & 4 deletions packages/data-schema/__tests__/ModelType.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expectTypeTestsToPassAsync } from 'jest-tsd';
import { a, ClientSchema } from '../src/index';
import { PrivateProviders, Operations, Operation } from '../src/Authorization';
import { configure } from '../src/ModelSchema';

describe('type definition tests', () => {
// evaluates type defs in corresponding test-d.ts file
Expand Down Expand Up @@ -899,10 +900,18 @@ describe('disableOperations', () => {
});

describe("default() to GQL mapping", () => {
const postgresConfig = configure({
database: {
identifier: 'some-identifier',
engine: 'postgresql',
connectionUri: '' as any,
},
})

it("should map .default(val) to `@default(value: val)`", () => {
const schema = a
.schema({
song: a
Song: a
.model({
title: a.string().default("Little Wing"),
})
Expand All @@ -913,12 +922,28 @@ describe("default() to GQL mapping", () => {
})

it("should map .default() to `@default`", () => {
const schema = a
const schema = postgresConfig
.schema({
Album: a
.model({
trackNumber: a.integer().default(),
title: a.string(),
})
})
.authorization((allow) => allow.publicApiKey());

expect(schema.transform().schema).toMatchSnapshot();
})

it("should map generated (`.default()`) identifiers to @primaryKey @default", () => {
const schema = postgresConfig
.schema({
song: a
Song: a
.model({
title: a.string().default(),
id: a.integer().default(),
title: a.string()
})
.identifier(["id"])
})
.authorization((allow) => allow.publicApiKey());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`default() to GQL mapping should map .default() to \`@default\` 1`] = `
"type song @model @auth(rules: [{allow: public, provider: apiKey}])
"type Album @model(timestamps: null) @auth(rules: [{allow: public, provider: apiKey}])
{
title: String @default
trackNumber: Int @default
title: String
}"
`;

exports[`default() to GQL mapping should map .default(val) to \`@default(value: val)\` 1`] = `
"type song @model @auth(rules: [{allow: public, provider: apiKey}])
"type Song @model @auth(rules: [{allow: public, provider: apiKey}])
{
title: String @default(value: "Little Wing")
}"
`;

exports[`default() to GQL mapping should map generated (\`.default()\`) identifiers to @primaryKey @default 1`] = `
"type Song @model(timestamps: null) @auth(rules: [{allow: public, provider: apiKey}])
{
id: Int! @primaryKey @default
title: String
}"
`;

exports[`disableOperations coarse grained op takes precedence over fine-grained 1`] = `
"type widget @model(mutations:null) @auth(rules: [{allow: public, provider: apiKey}])
{
Expand Down
2 changes: 1 addition & 1 deletion packages/data-schema/docs/data-schema.modeltype.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Model type definition interface
```typescript
export type ModelType<T extends ModelTypeParamShape = ModelTypeParamShape, UsedMethod extends UsableModelTypeKey = never> = Omit<{
[brandSymbol]: typeof brandName;
identifier<PrimaryIndexFields = ExtractSecondaryIndexIRFields<T, true>, 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'>;
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'>;
Expand Down
2 changes: 1 addition & 1 deletion packages/data-schema/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aws-amplify/data-schema",
"version": "1.10.2",
"version": "1.11.0",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand Down
9 changes: 2 additions & 7 deletions packages/data-schema/src/ModelType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,12 @@ export type ModelTypeParamShape = {
*/
export type ExtractSecondaryIndexIRFields<
T extends ModelTypeParamShape,
RequiredOnly extends boolean = false,
> = {
[FieldProp in keyof T['fields'] as T['fields'][FieldProp] extends BaseModelField<
infer R
>
? NonNullable<R> extends string | number
? RequiredOnly extends false
? FieldProp
: null extends R
? never
: FieldProp
? FieldProp
: never
: T['fields'][FieldProp] extends
| EnumType
Expand Down Expand Up @@ -230,7 +225,7 @@ export type ModelType<
{
[brandSymbol]: typeof brandName;
identifier<
PrimaryIndexFields = ExtractSecondaryIndexIRFields<T, true>,
PrimaryIndexFields = ExtractSecondaryIndexIRFields<T>,
PrimaryIndexPool extends string = keyof PrimaryIndexFields & string,
const ID extends ReadonlyArray<PrimaryIndexPool> = readonly [],
const PrimaryIndexIR extends PrimaryIndexIrShape = PrimaryIndexFieldsToIR<
Expand Down
56 changes: 55 additions & 1 deletion packages/data-schema/src/SchemaProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
FunctionSchemaAccess,
LambdaFunctionDefinition,
CustomSqlDataSourceStrategy,
DatasourceEngine,
} from '@aws-amplify/data-schema-types';
import type { InternalRef, RefType } from './RefType';
import type { EnumType } from './EnumType';
Expand Down Expand Up @@ -155,6 +156,12 @@ function isRefField(
return isRefFieldDef((field as any)?.data);
}

function canGenerateFieldType(
fieldType: ModelFieldType
): boolean {
return fieldType === 'Int';
}

function scalarFieldToGql(
fieldDef: ScalarFieldDef,
identifier?: readonly string[],
Expand All @@ -169,6 +176,7 @@ function scalarFieldToGql(
} = fieldDef;
let field: string = fieldType;


if (identifier !== undefined) {
field += '!';
if (identifier.length > 1) {
Expand All @@ -184,6 +192,10 @@ function scalarFieldToGql(
field += ` ${index}`;
}

if (_default === __generated) {
field += ` @default`;
}

return field;
}

Expand Down Expand Up @@ -918,6 +930,37 @@ function processFieldLevelAuthRules(
return fieldLevelAuthRules;
}

function validateDBGeneration(fields: Record<string, any>, databaseEngine: DatasourceEngine) {
for (const [fieldName, fieldDef] of Object.entries(fields)) {
const _default = fieldDef.data?.default;
const fieldType = fieldDef.data?.fieldType;
const isGenerated = _default === __generated;

if (isGenerated && databaseEngine !== 'postgresql') {
throw new Error(`Invalid field definition for ${fieldName}. DB-generated fields are only supported with PostgreSQL data sources.`);
}

if (isGenerated && !canGenerateFieldType(fieldType)) {
throw new Error(`Incompatible field type. Field type ${fieldType} in field ${fieldName} cannot be configured as a DB-generated field.`);
}
}
}

function validateNullableIdentifiers(fields: Record<string, any>, identifier?: readonly string[]){
for (const [fieldName, fieldDef] of Object.entries(fields)) {
const fieldType = fieldDef.data?.fieldType;
const required = fieldDef.data?.required;
const _default = fieldDef.data?.default;
const isGenerated = _default === __generated;

if (identifier !== undefined && identifier.includes(fieldName)) {
if (!required && fieldType !== 'ID' && !isGenerated) {
throw new Error(`Invalid identifier definition. Field ${fieldName} cannot be used in the identifier. Identifiers must reference required or DB-generated fields)`);
}
}
}
}

function processFields(
typeName: string,
fields: Record<string, any>,
Expand All @@ -926,13 +969,16 @@ function processFields(
identifier?: readonly string[],
partitionKey?: string,
secondaryIndexes: TransformedSecondaryIndexes = {},
databaseEngine: DatasourceEngine = 'dynamodb',
) {
const gqlFields: string[] = [];
// stores nested, field-level type definitions (custom types and enums)
// the need to be hoisted to top-level schema types and processed accordingly
const implicitTypes: [string, any][] = [];

validateImpliedFields(fields, impliedFields);
validateDBGeneration(fields, databaseEngine);
validateNullableIdentifiers(fields, identifier)

for (const [fieldName, fieldDef] of Object.entries(fields)) {
const fieldAuth = fieldLevelAuthRules[fieldName]
Expand Down Expand Up @@ -1289,8 +1335,9 @@ const schemaPreprocessor = (
const lambdaFunctions: LambdaFunctionDefinition = {};
const customSqlDataSourceStrategies: CustomSqlDataSourceStrategy[] = [];

const databaseEngine = schema.data.configuration.database.engine
const databaseType =
schema.data.configuration.database.engine === 'dynamodb'
databaseEngine === 'dynamodb'
? 'dynamodb'
: 'sql';

Expand Down Expand Up @@ -1373,6 +1420,10 @@ const schemaPreprocessor = (
fields,
authFields,
fieldLevelAuthRules,
undefined,
undefined,
undefined,
databaseEngine
);

topLevelTypes.push(...implicitTypes);
Expand Down Expand Up @@ -1484,6 +1535,8 @@ const schemaPreprocessor = (
fieldLevelAuthRules,
identifier,
partitionKey,
undefined,
databaseEngine,
);

topLevelTypes.push(...implicitTypes);
Expand Down Expand Up @@ -1548,6 +1601,7 @@ const schemaPreprocessor = (
identifier,
partitionKey,
transformedSecondaryIndexes,
databaseEngine,
);
topLevelTypes.push(...implicitTypes);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Primary Indexes Custom identifier single field SK defaulted/serial field can be used in identifier 1`] = `
"type Model @model(timestamps: null) @auth(rules: [{allow: public, provider: apiKey}])
{
idSerial: Int! @primaryKey @default
content: String
}"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`validated on execution Generated fields and identifiers: datetime Throws if identifier is nullable and has no default 1`] = `"Invalid identifier definition. Field nullableFieldNoDefault cannot be used in the identifier. Identifiers must reference required or DB-generated fields)"`;

exports[`validated on execution Generated fields and identifiers: datetime Throws if you use \`.default()\` on unsupported engine 1`] = `"Invalid field definition for nullableGeneratedField. DB-generated fields are only supported with PostgreSQL data sources."`;

exports[`validated on execution Generated fields and identifiers: datetime Throws if you use \`.default()\` on unsupported engine 2`] = `"Model \`Model\` is missing authorization rules. Add global rules to the schema or ensure every model has its own rules."`;

exports[`validated on execution Generated fields and identifiers: datetime Throws if you use \`.default()\` on unsupported engine 3`] = `"Incompatible field type. Field type AWSDateTime in field nullableGeneratedField cannot be configured as a DB-generated field."`;

exports[`validated on execution Generated fields and identifiers: datetime Throws if you use \`.default()\` on unsupported field type 1`] = `"Incompatible field type. Field type AWSDateTime in field nullableGeneratedField cannot be configured as a DB-generated field."`;

exports[`validated on execution Generated fields and identifiers: float Throws if identifier is nullable and has no default 1`] = `"Invalid identifier definition. Field nullableFieldNoDefault cannot be used in the identifier. Identifiers must reference required or DB-generated fields)"`;

exports[`validated on execution Generated fields and identifiers: float Throws if you use \`.default()\` on unsupported engine 1`] = `"Invalid field definition for nullableGeneratedField. DB-generated fields are only supported with PostgreSQL data sources."`;

exports[`validated on execution Generated fields and identifiers: float Throws if you use \`.default()\` on unsupported engine 2`] = `"Model \`Model\` is missing authorization rules. Add global rules to the schema or ensure every model has its own rules."`;

exports[`validated on execution Generated fields and identifiers: float Throws if you use \`.default()\` on unsupported engine 3`] = `"Incompatible field type. Field type Float in field nullableGeneratedField cannot be configured as a DB-generated field."`;

exports[`validated on execution Generated fields and identifiers: float Throws if you use \`.default()\` on unsupported field type 1`] = `"Incompatible field type. Field type Float in field nullableGeneratedField cannot be configured as a DB-generated field."`;

exports[`validated on execution Generated fields and identifiers: integer Throws if identifier is nullable and has no default 1`] = `"Invalid identifier definition. Field nullableFieldNoDefault cannot be used in the identifier. Identifiers must reference required or DB-generated fields)"`;

exports[`validated on execution Generated fields and identifiers: integer Throws if you use \`.default()\` on unsupported engine 1`] = `"Invalid field definition for nullableGeneratedField. DB-generated fields are only supported with PostgreSQL data sources."`;

exports[`validated on execution Generated fields and identifiers: integer Throws if you use \`.default()\` on unsupported engine 2`] = `"Model \`Model\` is missing authorization rules. Add global rules to the schema or ensure every model has its own rules."`;

exports[`validated on execution Generated fields and identifiers: string Throws if identifier is nullable and has no default 1`] = `"Invalid identifier definition. Field nullableFieldNoDefault cannot be used in the identifier. Identifiers must reference required or DB-generated fields)"`;

exports[`validated on execution Generated fields and identifiers: string Throws if you use \`.default()\` on unsupported engine 1`] = `"Invalid field definition for nullableGeneratedField. DB-generated fields are only supported with PostgreSQL data sources."`;

exports[`validated on execution Generated fields and identifiers: string Throws if you use \`.default()\` on unsupported engine 2`] = `"Model \`Model\` is missing authorization rules. Add global rules to the schema or ensure every model has its own rules."`;

exports[`validated on execution Generated fields and identifiers: string Throws if you use \`.default()\` on unsupported engine 3`] = `"Incompatible field type. Field type String in field nullableGeneratedField cannot be configured as a DB-generated field."`;

exports[`validated on execution Generated fields and identifiers: string Throws if you use \`.default()\` on unsupported field type 1`] = `"Incompatible field type. Field type String in field nullableGeneratedField cannot be configured as a DB-generated field."`;

exports[`validated on execution Generated fields and identifiers: timestamp Throws if identifier is nullable and has no default 1`] = `"Invalid identifier definition. Field nullableFieldNoDefault cannot be used in the identifier. Identifiers must reference required or DB-generated fields)"`;

exports[`validated on execution Generated fields and identifiers: timestamp Throws if you use \`.default()\` on unsupported engine 1`] = `"Invalid field definition for nullableGeneratedField. DB-generated fields are only supported with PostgreSQL data sources."`;

exports[`validated on execution Generated fields and identifiers: timestamp Throws if you use \`.default()\` on unsupported engine 2`] = `"Model \`Model\` is missing authorization rules. Add global rules to the schema or ensure every model has its own rules."`;

exports[`validated on execution Generated fields and identifiers: timestamp Throws if you use \`.default()\` on unsupported engine 3`] = `"Incompatible field type. Field type AWSTimestamp in field nullableGeneratedField cannot be configured as a DB-generated field."`;

exports[`validated on execution Generated fields and identifiers: timestamp Throws if you use \`.default()\` on unsupported field type 1`] = `"Incompatible field type. Field type AWSTimestamp in field nullableGeneratedField cannot be configured as a DB-generated field."`;
Loading

0 comments on commit aa4c942

Please sign in to comment.