From 79953c5d7dfd20d68238089fe9900532f1e9e0e6 Mon Sep 17 00:00:00 2001 From: Hein Jeong <73264629+hein-j@users.noreply.github.com> Date: Wed, 13 Apr 2022 09:45:30 -0700 Subject: [PATCH] feat: support type casting for DataStore hooks (#460) Co-authored-by: Hein Jeong --- .../studio-ui-codegen-react.test.ts.snap | 10 ++++++ .../lib/imports/import-mapping.ts | 1 + .../codegen-ui-react/lib/workflow/action.ts | 32 +++++++++++------ .../cypress/integration/workflow-spec.ts | 19 ++++++----- .../src/ComponentTests.tsx | 8 ++++- .../src/WorkflowTests.tsx | 34 ++++++++++++------- .../collections/simpleUserCollection.json | 18 ++++++++++ .../components/workflow/dataStoreActions.json | 12 +++++++ 8 files changed, 102 insertions(+), 32 deletions(-) diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap index fddead2d3..01ede4920 100644 --- a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap +++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react.test.ts.snap @@ -10,6 +10,7 @@ import { useDataStoreCreateAction, } from \\"@aws-amplify/ui-react/internal\\"; import { Customer } from \\"../models\\"; +import { schema } from \\"../models/schema\\"; import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type CreateCustomerButtonProps = React.PropsWithChildren< @@ -24,6 +25,7 @@ export default function CreateCustomerButton( const createCustomerButtonOnClick = useDataStoreCreateAction({ model: Customer, fields: { firstName: \\"Din\\", lastName: \\"Djarin\\" }, + schema: schema, }); return ( /* @ts-ignore: TS2322 */ @@ -53,6 +55,7 @@ import { useDataStoreDeleteAction, } from \\"@aws-amplify/ui-react/internal\\"; import { Customer } from \\"../models\\"; +import { schema } from \\"../models/schema\\"; import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type DeleteCustomerButtonProps = React.PropsWithChildren< @@ -67,6 +70,7 @@ export default function DeleteCustomerButton( const deleteCustomerButtonOnClick = useDataStoreDeleteAction({ model: Customer, id: \\"d9887268-47dd-4899-9568-db5809218751\\", + schema: schema, }); return ( /* @ts-ignore: TS2322 */ @@ -96,6 +100,7 @@ import { useDataStoreUpdateAction, } from \\"@aws-amplify/ui-react/internal\\"; import { Customer } from \\"../models\\"; +import { schema } from \\"../models/schema\\"; import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type UpdateCustomerButtonProps = React.PropsWithChildren< @@ -111,6 +116,7 @@ export default function UpdateCustomerButton( model: Customer, id: \\"d9887268-47dd-4899-9568-db5809218751\\", fields: { firstName: \\"Din\\", lastName: \\"Djarin\\" }, + schema: schema, }); return ( /* @ts-ignore: TS2322 */ @@ -503,6 +509,7 @@ import { useDataStoreCreateAction, } from \\"@aws-amplify/ui-react/internal\\"; import { Customer } from \\"../models\\"; +import { schema } from \\"../models/schema\\"; import { Button, ButtonProps } from \\"@aws-amplify/ui-react\\"; export type ComponentWithAuthEventBindingProps = React.PropsWithChildren< @@ -521,6 +528,7 @@ export default function ComponentWithAuthEventBinding( userName: authAttributes[\\"username\\"], favoriteIceCream: authAttributes[\\"custom:favorite_icecream\\"], }, + schema: schema, }); return ( /* @ts-ignore: TS2322 */ @@ -5473,6 +5481,7 @@ import { useStateMutationAction, } from \\"@aws-amplify/ui-react/internal\\"; import { Customer } from \\"../models\\"; +import { schema } from \\"../models/schema\\"; import { Button, Flex, FlexProps, TextField } from \\"@aws-amplify/ui-react\\"; export type MyFormProps = React.PropsWithChildren< @@ -5488,6 +5497,7 @@ export default function MyForm(props: MyFormProps): React.ReactElement { model: Customer, id: \\"d9887268-47dd-4899-9568-db5809218751\\", fields: { username: usernameTextFieldValue }, + schema: schema, }); return ( /* @ts-ignore: TS2322 */ diff --git a/packages/codegen-ui-react/lib/imports/import-mapping.ts b/packages/codegen-ui-react/lib/imports/import-mapping.ts index b6bffab62..72c4ea9e5 100644 --- a/packages/codegen-ui-react/lib/imports/import-mapping.ts +++ b/packages/codegen-ui-react/lib/imports/import-mapping.ts @@ -19,6 +19,7 @@ export enum ImportSource { UI_REACT_INTERNAL = '@aws-amplify/ui-react/internal', AMPLIFY_DATASTORE = '@aws-amplify/datastore', LOCAL_MODELS = '../models', + LOCAL_SCHEMA = '../models/schema', } export enum ImportValue { diff --git a/packages/codegen-ui-react/lib/workflow/action.ts b/packages/codegen-ui-react/lib/workflow/action.ts index d423f92d0..040e95c12 100644 --- a/packages/codegen-ui-react/lib/workflow/action.ts +++ b/packages/codegen-ui-react/lib/workflow/action.ts @@ -48,6 +48,12 @@ export const ActionNameMapping: Partial> = { [Action['Amplify.Mutation']]: ImportValue.USE_STATE_MUTATION_ACTION, }; +const DataStoreActions = new Set([ + Action['Amplify.DataStoreCreateItemAction'], + Action['Amplify.DataStoreDeleteItemAction'], + Action['Amplify.DataStoreUpdateItemAction'], +]); + function isMutationAction(action: ActionStudioComponentEvent): action is MutationAction { return (action.action as Action) === Action['Amplify.Mutation']; } @@ -172,22 +178,28 @@ export function buildActionArguments( importCollection: ImportCollection, ): ObjectLiteralExpression[] | undefined { if (action.parameters) { - return [ - factory.createObjectLiteralExpression( - Object.entries(action.parameters).map(([key, value]) => - factory.createPropertyAssignment( - factory.createIdentifier(key), - getActionParameterValue(componentMetadata, key, value, importCollection), - ), - ), - false, + const properties = Object.entries(action.parameters).map(([key, value]) => + factory.createPropertyAssignment( + factory.createIdentifier(key), + getActionParameterValue(componentMetadata, key, value, importCollection), ), - ]; + ); + + if (DataStoreActions.has(action.action as Action)) { + addSchemaToArguments(properties, importCollection); + } + return [factory.createObjectLiteralExpression(properties, false)]; } return undefined; } +export function addSchemaToArguments(properties: ts.PropertyAssignment[], importCollection: ImportCollection) { + const SCHEMA = 'schema'; + properties.push(factory.createPropertyAssignment(factory.createIdentifier(SCHEMA), factory.createIdentifier(SCHEMA))); + importCollection.addImport(ImportSource.LOCAL_SCHEMA, SCHEMA); +} + export function getActionParameterValue( componentMetadata: ComponentMetadata, key: string, diff --git a/packages/test-generator/integration-test-templates/cypress/integration/workflow-spec.ts b/packages/test-generator/integration-test-templates/cypress/integration/workflow-spec.ts index de8bfae99..9e70950e8 100644 --- a/packages/test-generator/integration-test-templates/cypress/integration/workflow-spec.ts +++ b/packages/test-generator/integration-test-templates/cypress/integration/workflow-spec.ts @@ -167,18 +167,21 @@ describe('Workflow', () => { }); describe('DataStore', () => { - it('supports creating a datastore item', () => { - cy.get('#user-collection').contains('Din Djarin').should('not.exist'); + it('supports creating a datastore item, type-casting scalar values', () => { + const expected = 'Din Djarin | age: 200 | isLoggedIn: true'; + cy.get('#user-collection').contains(expected).should('not.exist'); cy.get('#create-item').click(); - cy.get('#user-collection').contains('Din Djarin'); + cy.get('#user-collection').contains(expected); }); - it('supports updating a datastore item', () => { - cy.get('#user-collection').contains('UpdateMe Me'); - cy.get('#user-collection').contains('Moff Gideon').should('not.exist'); + it('supports updating a datastore item, type-casting scalar values', () => { + const before = 'UpdateMe Me'; + const after = 'Moff Gideon | age: 200 | isLoggedIn: true'; + cy.get('#user-collection').contains(before); + cy.get('#user-collection').contains(after).should('not.exist'); cy.get('#update-item').click(); - cy.get('#user-collection').contains('UpdateMe Me').should('not.exist'); - cy.get('#user-collection').contains('Moff Gideon'); + cy.get('#user-collection').contains(before).should('not.exist'); + cy.get('#user-collection').contains(after); }); it('supports deleting a datastore item', () => { diff --git a/packages/test-generator/integration-test-templates/src/ComponentTests.tsx b/packages/test-generator/integration-test-templates/src/ComponentTests.tsx index d4a718efc..b71db4bc3 100644 --- a/packages/test-generator/integration-test-templates/src/ComponentTests.tsx +++ b/packages/test-generator/integration-test-templates/src/ComponentTests.tsx @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { AmplifyProvider } from '@aws-amplify/ui-react'; import '@aws-amplify/ui-react/styles.css'; import { DataStore } from 'aws-amplify'; @@ -128,8 +128,13 @@ const initializeListingTestData = async (): Promise => { export default function ComponentTests() { const [isInitialized, setInitialized] = useState(false); + const initializeStarted = useRef(false); + useEffect(() => { const initializeTestUserData = async () => { + if (initializeStarted.current) { + return; + } // DataStore.clear() doesn't appear to reliably work in this scenario. indexedDB.deleteDatabase('amplify-datastore'); await Promise.all([initializeUserTestData(), initializeListingTestData()]); @@ -142,6 +147,7 @@ export default function ComponentTests() { }; initializeTestUserData(); + initializeStarted.current = true; }, []); if (!isInitialized) { diff --git a/packages/test-generator/integration-test-templates/src/WorkflowTests.tsx b/packages/test-generator/integration-test-templates/src/WorkflowTests.tsx index 56c809691..a07a473c7 100644 --- a/packages/test-generator/integration-test-templates/src/WorkflowTests.tsx +++ b/packages/test-generator/integration-test-templates/src/WorkflowTests.tsx @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useState, SyntheticEvent, useEffect } from 'react'; +import { useState, SyntheticEvent, useEffect, useRef } from 'react'; import '@aws-amplify/ui-react/styles.css'; import { AmplifyProvider, View, Heading, Divider, Button } from '@aws-amplify/ui-react'; import { Hub } from 'aws-amplify'; @@ -61,6 +61,7 @@ export default function ComplexTests() { const [hasDisappeared, setDisappeared] = useState(false); const [authState, setAuthState] = useState('LoggedIn'); const [formIdToUpdate, setFormIdToUpdate] = useState(''); + const initializeStarted = useRef(false); const initializeUserTestData = async (): Promise => { await DataStore.save(new User({ firstName: 'DeleteMe', lastName: 'Me' })); @@ -88,22 +89,29 @@ export default function ComplexTests() { useEffect(() => { const initializeTestState = async () => { + if (initializeStarted.current) { + return; + } // DataStore.clear() doesn't appear to reliably work in this scenario. - indexedDB.deleteDatabase('amplify-datastore'); - await initializeUserTestData(); - const queriedIdToDelete = (await DataStore.query(User, (criteria) => criteria.firstName('eq', 'DeleteMe')))[0].id; - setIdToDelete(queriedIdToDelete); - const queriedIdToUpdate = (await DataStore.query(User, (criteria) => criteria.firstName('eq', 'UpdateMe')))[0].id; - setIdToUpdate(queriedIdToUpdate); - const queriedFormIdToUpdate = ( - await DataStore.query(User, (criteria) => criteria.firstName('eq', 'FormUpdate')) - )[0].id; - setFormIdToUpdate(queriedFormIdToUpdate); - initializeAuthListener(); - setInitialized(true); + indexedDB.deleteDatabase('amplify-datastore').onsuccess = async function () { + await initializeUserTestData(); + const queriedIdToDelete = (await DataStore.query(User, (criteria) => criteria.firstName('eq', 'DeleteMe')))[0] + .id; + setIdToDelete(queriedIdToDelete); + const queriedIdToUpdate = (await DataStore.query(User, (criteria) => criteria.firstName('eq', 'UpdateMe')))[0] + .id; + setIdToUpdate(queriedIdToUpdate); + const queriedFormIdToUpdate = ( + await DataStore.query(User, (criteria) => criteria.firstName('eq', 'FormUpdate')) + )[0].id; + setFormIdToUpdate(queriedFormIdToUpdate); + initializeAuthListener(); + setInitialized(true); + }; }; initializeTestState(); + initializeStarted.current = true; }, []); const complexModels = useDataStoreBinding({ diff --git a/packages/test-generator/lib/components/collections/simpleUserCollection.json b/packages/test-generator/lib/components/collections/simpleUserCollection.json index 94df8e318..bb6077be0 100644 --- a/packages/test-generator/lib/components/collections/simpleUserCollection.json +++ b/packages/test-generator/lib/components/collections/simpleUserCollection.json @@ -34,6 +34,24 @@ "property": "user", "field": "lastName" } + }, + { + "value": " | age: " + }, + { + "collectionBindingProperties": { + "property": "user", + "field": "age" + } + }, + { + "value": " | isLoggedIn: " + }, + { + "collectionBindingProperties": { + "property": "user", + "field": "isLoggedIn" + } } ] } diff --git a/packages/test-generator/lib/components/workflow/dataStoreActions.json b/packages/test-generator/lib/components/workflow/dataStoreActions.json index f1944a479..a8668a9d4 100644 --- a/packages/test-generator/lib/components/workflow/dataStoreActions.json +++ b/packages/test-generator/lib/components/workflow/dataStoreActions.json @@ -33,6 +33,12 @@ }, "lastName": { "value": "Djarin" + }, + "isLoggedIn": { + "value": "true" + }, + "age": { + "value": "200" } } } @@ -66,6 +72,12 @@ }, "lastName": { "value": "Gideon" + }, + "isLoggedIn": { + "value": "true" + }, + "age": { + "value": "200" } } }