Skip to content

Commit

Permalink
feat: auto increment support (#2883)
Browse files Browse the repository at this point in the history
* chore(graphql-default-value-transformer): tidy tests

* test(graphql-default-value-transformer): add unit tests for auto increment support

* feat: 🎸 utils to detect Postgres datasource

* feat: 🎸 support auto increment

Implements support for auto increment (serial) fields from Postgres
datasources. Such fields are denoted by an empty `@default` applied to
an `Int` field.

* test(graphql-default-value-transformer): pk can be auto increment

* test(graphql-default-value-transformer): auto-increment crud e2e

* chore: describe test purpose

* chore: removing logging

* chore: describe why invalid cases are invalid

* chore: remove unecessary e2e test case

* chore: test messaging clarity

* chore: type safety

* chore: alphabetize list

* chore: type of return value asserts against string

Co-authored-by: Tim Schmelter <schmelte+github@amazon.com>

* chore: test ensures customers can insert to serial fields with custom values

* chore: verify that @default(value) works on mysql

* chore: remove unecessary ssm test case

* chore: update branch from main

* test: value cannot be null on ddb

---------

Co-authored-by: Tim Schmelter <schmelte+github@amazon.com>
  • Loading branch information
2 people authored and tejas2008 committed Oct 29, 2024
1 parent 2564460 commit 5f2c4f7
Show file tree
Hide file tree
Showing 16 changed files with 963 additions and 240 deletions.
136 changes: 73 additions & 63 deletions codebuild_specs/e2e_workflow.yml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import generator from 'generate-password';
import { getResourceNamesForStrategyName, ImportedRDSType } from '@aws-amplify/graphql-transformer-core';
import { getRDSTableNamePrefix } from 'amplify-category-api-e2e-core';
import { SqlDatatabaseController } from '../sql-datatabase-controller';
import { DURATION_1_HOUR } from '../utils/duration-constants';
import { testGraphQLAPIAutoIncrement } from '../sql-tests-common/sql-models-auto-increment';

jest.setTimeout(DURATION_1_HOUR);

describe('CDK GraphQL Transformer deployments with Postgres SQL datasources', () => {
const projFolderName = 'pgmodels';

// sufficient password length that meets the requirements for RDS cluster/instance
const [username, password, identifier] = generator.generateMultiple(3, { length: 11 });
const region = process.env.CLI_REGION ?? 'us-west-2';
const engine = 'postgres';

const databaseController: SqlDatatabaseController = new SqlDatatabaseController(
[
`CREATE TABLE "${getRDSTableNamePrefix()}coffee_queue" ("orderNumber" SERIAL PRIMARY KEY, "order" VARCHAR(256) NOT NULL, "customer" VARCHAR(256))`,
],
{
identifier,
engine,
username,
password,
region,
},
);

const strategyName = `${engine}DBStrategy`;
const resourceNames = getResourceNamesForStrategyName(strategyName);

beforeAll(async () => {
await databaseController.setupDatabase();
});

afterAll(async () => {
await databaseController.cleanupDatabase();
});

const constructTestOptions = (connectionConfigName: string) => ({
projFolderName,
region,
connectionConfigName,
dbController: databaseController,
resourceNames,
});

testGraphQLAPIAutoIncrement(
constructTestOptions('connectionUri'),
'creates a GraphQL API from SQL-based models using Connection String SSM parameter',
ImportedRDSType.POSTGRESQL,
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FieldMap } from '../../../utils/sql-crudl-helper';

export const coffeeQueueFieldMap: FieldMap = {
orderNumber: true,
order: true,
customer: true,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type CoffeeQueue @model @refersTo(name: "e2e_test_coffee_queue") {
orderNumber: Int! @primaryKey @default
order: String!
customer: String
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
type Todo @model @refersTo(name: "e2e_test_todos") {
id: ID! @primaryKey
description: String!
description: String! @default(value: "Lorem ipsum yadda yadda...")
}
type Student @model @refersTo(name: "e2e_test_students") {
studentId: Int! @primaryKey(sortKeyFields: ["classId"])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import { ImportedRDSType } from '@aws-amplify/graphql-transformer-core';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import { createNewProjectDir, deleteProjectDir, getRDSTableNamePrefix } from 'amplify-category-api-e2e-core';
import { initCDKProject, cdkDeploy, cdkDestroy } from '../commands';
import { SqlDatatabaseController } from '../sql-datatabase-controller';
import { CRUDLHelper } from '../utils/sql-crudl-helper';
import { ONE_MINUTE } from '../utils/duration-constants';
import { coffeeQueueFieldMap } from './schemas/sql-auto-increment/field-map';

export const testGraphQLAPIAutoIncrement = (
options: {
projFolderName: string;
region: string;
connectionConfigName: string;
dbController: SqlDatatabaseController;
resourceNames: { sqlLambdaAliasName: string };
},
testBlockDescription: string,
engine: ImportedRDSType,
): void => {
describe(`${testBlockDescription} - ${engine}`, () => {
// In particular, we want to verify that the new CREATE operation
// is allowed to omit the primary key field, and that the primary key
// we get back is the correct, db generated value.
// NOTE: Expects underlying orderNumber column to be a serial primary key in Postgres table
const schemaPath = path.resolve(path.join(__dirname, '..', 'sql-tests-common', 'schemas', 'sql-auto-increment', 'schema.graphql'));
const schemaConfigString = fs.readFileSync(schemaPath).toString();
const { projFolderName, region, connectionConfigName, dbController } = options;
const templatePath = path.resolve(path.join(__dirname, '..', '__tests__', 'backends', 'sql-models'));

let projRoot: string;
let name: string;
let outputs: Promise<any>;
let coffeeQueueTableCRUDLHelper: CRUDLHelper;

beforeAll(async () => {
projRoot = await createNewProjectDir(projFolderName);
name = await initCDKProject(projRoot, templatePath);
dbController.writeDbDetails(projRoot, connectionConfigName, schemaConfigString);
outputs = await cdkDeploy(projRoot, '--all', { postDeployWaitMs: ONE_MINUTE });
const { awsAppsyncApiEndpoint: apiEndpoint, awsAppsyncApiKey: apiKey } = outputs[name];

const appSyncClient = new AWSAppSyncClient({
url: apiEndpoint,
region,
disableOffline: true,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey,
},
});

coffeeQueueTableCRUDLHelper = new CRUDLHelper(appSyncClient, 'CoffeeQueue', 'CoffeeQueues', coffeeQueueFieldMap);
});

afterAll(async () => {
try {
await cdkDestroy(projRoot, '--all');
await dbController.clearDatabase();
} catch (err) {
console.log(`Error invoking 'cdk destroy': ${err}`);
}

deleteProjectDir(projRoot);
});

test(`check CRUDL on coffee queue table with auto increment primary key - ${engine}`, async () => {
// Order Coffee Mutation
const createCoffeeOrder1 = await coffeeQueueTableCRUDLHelper.create({ customer: 'petesv', order: 'cold brew' });

expect(createCoffeeOrder1).toBeDefined();
expect(createCoffeeOrder1.orderNumber).toBeDefined();
expect(createCoffeeOrder1.customer).toEqual('petesv');
expect(createCoffeeOrder1.order).toEqual('cold brew');

// Get Todo Query
const getCoffeeOrder1 = await coffeeQueueTableCRUDLHelper.get({ orderNumber: createCoffeeOrder1.orderNumber });

expect(getCoffeeOrder1.orderNumber).toEqual(createCoffeeOrder1.orderNumber);
expect(getCoffeeOrder1.customer).toEqual(createCoffeeOrder1.customer);

// Update Todo Mutation
const updateCoffeeOrder1 = await coffeeQueueTableCRUDLHelper.update({
orderNumber: createCoffeeOrder1.orderNumber,
customer: 'petesv',
order: 'hot brew',
});

expect(updateCoffeeOrder1.orderNumber).toEqual(createCoffeeOrder1.orderNumber);
expect(updateCoffeeOrder1.order).toEqual('hot brew');

// Get Todo Query after update
const getUpdatedCoffeeOrder1 = await coffeeQueueTableCRUDLHelper.get({ orderNumber: createCoffeeOrder1.orderNumber });

expect(getUpdatedCoffeeOrder1.orderNumber).toEqual(createCoffeeOrder1.orderNumber);
expect(getUpdatedCoffeeOrder1.order).toEqual('hot brew');

// List Todo Query & Create with custom SERIAL field value
const customOrderNumber = 42;
const createCofffeeOrder2 = await coffeeQueueTableCRUDLHelper.create({ orderNumber: customOrderNumber, order: 'latte' });
expect(createCofffeeOrder2.orderNumber).toEqual(customOrderNumber);

const listTodo = await coffeeQueueTableCRUDLHelper.list();
expect(listTodo.items.length).toEqual(2);
expect(listTodo.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
orderNumber: getUpdatedCoffeeOrder1.orderNumber,
order: 'hot brew',
}),
expect.objectContaining({
orderNumber: createCofffeeOrder2.orderNumber,
order: 'latte',
}),
]),
);

// Delete Todo Mutation
const deleteCoffeeOrder1 = await coffeeQueueTableCRUDLHelper.delete({ orderNumber: createCoffeeOrder1.orderNumber });

expect(deleteCoffeeOrder1.orderNumber).toEqual(createCoffeeOrder1.orderNumber);
expect(deleteCoffeeOrder1.order).toEqual('hot brew');

const getDeletedCoffeeOrder1 = await coffeeQueueTableCRUDLHelper.get({ orderNumber: createCoffeeOrder1.orderNumber });

expect(getDeletedCoffeeOrder1).toBeNull();

// List Todo Query after delete
const listCoffeeOrdersAfterDelete = await coffeeQueueTableCRUDLHelper.list();

expect(listCoffeeOrdersAfterDelete.items.length).toEqual(1);
expect(listCoffeeOrdersAfterDelete.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
orderNumber: createCofffeeOrder2.orderNumber,
order: 'latte',
}),
]),
);

// Check invalid CRUD operation returns generic error message
const createTodo6 = await coffeeQueueTableCRUDLHelper.create({ order: 'mocha' });

try {
// Invalid because the pk (orderNumber) already exists
await coffeeQueueTableCRUDLHelper.create({ orderNumber: createTodo6.orderNumber, order: 'americano' });
} catch (error) {
coffeeQueueTableCRUDLHelper.checkGenericError(error?.message);
}

const biggerThanAnyExistingOrderNumber = 99999999;

try {
await coffeeQueueTableCRUDLHelper.get({ orderNumber: biggerThanAnyExistingOrderNumber });
} catch (error) {
coffeeQueueTableCRUDLHelper.checkGenericError(error?.message);
}

try {
await coffeeQueueTableCRUDLHelper.update({ orderNumber: biggerThanAnyExistingOrderNumber, order: 'cortado' });
} catch (error) {
coffeeQueueTableCRUDLHelper.checkGenericError(error?.message);
}

try {
await coffeeQueueTableCRUDLHelper.delete({ orderNumber: biggerThanAnyExistingOrderNumber });
} catch (error) {
coffeeQueueTableCRUDLHelper.checkGenericError(error?.message);
}
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,34 @@ export const testGraphQLAPI = (
deleteProjectDir(projRoot);
});

test(`check default value on todo table - ${engine}`, async () => {
const defaultTodoDescription = 'Lorem ipsum yadda yadda...';

// Create Todo Mutation
const createTodo1 = await toDoTableCRUDLHelper.create({});

expect(createTodo1).toBeDefined();
expect(createTodo1.id).toBeDefined();
expect(createTodo1.description).toEqual(defaultTodoDescription);

// Get Todo Query
const getTodo1 = await toDoTableCRUDLHelper.getById(createTodo1.id);
expect(getTodo1.id).toEqual(createTodo1.id);
expect(getTodo1.description).toEqual(createTodo1.description);

// Update Todo Mutation
const updateTodo1 = await toDoTableCRUDLHelper.update({ id: createTodo1.id, description: 'Updated Todo #1' });

expect(updateTodo1.id).toEqual(createTodo1.id);
expect(updateTodo1.description).toEqual('Updated Todo #1');

// Get Todo Query after update
const getUpdatedTodo1 = await toDoTableCRUDLHelper.getById(createTodo1.id);

expect(getUpdatedTodo1.id).toEqual(createTodo1.id);
expect(getUpdatedTodo1.description).toEqual('Updated Todo #1');
});

test(`check CRUDL on todo table with default primary key - ${engine}`, async () => {
// Create Todo Mutation
const createTodo1 = await toDoTableCRUDLHelper.create({ description: 'Todo #1' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
"src/**/*.ts"
],
"coveragePathIgnorePatterns": [
"/__tests__/"
"/__tests__/",
"types.ts"
],
"snapshotFormat": {
"escapeString": true,
Expand Down
Loading

0 comments on commit 5f2c4f7

Please sign in to comment.