0.1.0 repo

Deprecated Archived content; some links may not be available.

This implementation deploys the Awayto project on an AWS based stack.

🔗 Overview

Quick Installation

Make sure your console session can perform AWS CLI commands with Administrator role access.

npm i -g @keybittech/awayto

mkdir proj
cd proj
awayto install <name> <environment> "<description>"

Prerequisites

Any AWS tasks are performed on an AWS account with the Administrator role.

  • An AWS Account

  • AWS CLI 2.0.42

  • Node 16.3.0

  • NPM 7.15.1

  • Postgres 13.4

  • Python 3.7.9

Utility Commands

  • npm run start – Start a local dev server at localhost:3000 which only serves the webapp, and uses the settings.development.env file.

  • npm run start-stack – Same as above, but if you opted for local-testing in the installation, this will start sam local using the env.json and template.sam.yaml files in the main directory. SAM Local starts up a docker contanerized instance of your Lambda function at localhost:3001. The webapp will pick up the settings.local.env file in this case.

  • npm run start-api – Starts just the api with the same above configuration.

  • npm run start-local – Starts just the webapp with the settings.local.env configuration. We use AWS SAM to run the local API, this requires Docker. AWS SAM, while extremely convenient, has gone through some growing pains; so if you ever run into issues, ask on the Discord. We use the --warm-containers LAZY option, so when your api starts make sure to make a couple requests to get the Lambda initialized.

  • npm run watch-api – Start a webpack watcher on just the api.

  • npm run build-api – Build the api with webpack using api.webpack.js. As a result of the build, a minified index.js containing the Lambda handler will be placed into the apipkg folder.

  • npm run build-web – Build the webapp using react-app-rewired. As a result of this build, a build folder will be generated.

  • npm run build-deploy – Build both the api and webapp, in the event you are preparing to deploy a full stack update.

  • npm run install-stack – In the event you are installing a re-packaged version of Awayto, you can use this command to install the related AWS resources into your own AWS account.

  • npm run db-create-migration <name> – Autogenerate a migration in the src/api/scripts/db folder.

  • npm run db-update – Deploy any un-deployed .sql files in the src/api/scripts/db folder. You can see what’s been deployed by reviewing the seed file bin/data/seeds.

  • npm run db-update-file <name> – If a script fails while updating, you can fix it then specifically-redeploy it with this command.

  • npm run invoke-event <name> – Use an event from src/api/scripts/events and run it against the live deployed lambda for your Awayto install.

  • npm run invoke-event-local <name> – Run Lambda events locally using AWS SAM.

  • npm run release – Run a release script which will deploy both the api (apipkg folder) and webapp (build folder) to s3. Then the script will request a CloudFront distribution invalidation on the entire webapp bucket. As well, the Lambda function will be re-deployed with the built handler.

Typescript Suite

Type reference can be found here.

Hooks

There are a few hooks offered out of the box, core to the Awayto UI design experience.

useRedux

Hook into the fully typed redux store. Access your redux state using this hook.

import { useRedux } from 'awayto';

const profile = useRedux(state => state.profile); // e.g. returns the currently logged in IUserProfile

useAct

useAct is a wrapper for dispatching actions. Give it an IActionTypes, a loader boolean, and the action payload if necessary.

import { useAct, IUtilActions } from 'awayto';

const { SET_SNACK } = IUtilActions;

const act = useAct();

act(SET_SNACK, { snackOn: 'Success!', snackType: 'success' }); // e.g. send a notification to the toast popup component

useApi

The useApi hook provides type-bound api functionality. By passing in a IActionTypes (e.g. IUtilActionTypes, IManageUsersActionTypes, etc…) we can control the structure of the api request, and more easily handle it on the backend.

import { useApi, IManageUsersActions, useRedux } from 'awayto';

const { GET_MANAGE_USERS, GET_MANAGE_USERS_BY_ID } = IManageUsersActions;

const api = useApi();
const users = useRedux(state => state.users);

api(GET_MANAGE_USERS); // Kickoff a GET/manage/users call, which will populate our redux state later

As long as we have setup our model, GET_MANAGE_USERS will inform the system of the API endpoint and shape of the request/response.

If the endpoint takes path parameters (like GET/manage/user/id/:id), or if the request requires a body, we can pass these as the third parameter. Pass a boolean as the second argument to show or hide a loading screen.

api(GET_MANAGE_USERS_BY_ID, false, { id }); // Get a user profile by a specified ID, refresh it in redux state, and do not show a loading screen

useCognitoUser

Use this hook to get access to Cognito functionality once the user has logged in, or to check if the user is logged in.

import { useCognitoUser } from 'awayto';

const cognitoUser = useCognitoUser();

await cognitoUser.getSession();

cognitoUser.isLoggedIn == true

useComponents

useComponents takes advantage of React.lazy as well as the Proxy API. By combining these functionalities, we end up with a tool that makes it easy to work in an expanding and quickly changing code base.

As new files are added to the project while the developer is working, they will be automatically discovered and made lazily available through useComponents. The only need is to refresh the browser page once the component has been added to the render cycle. If the developer tries to destructure a component that does not exist in the group of lazy loaded components, useComponents will return an empty div. This is configurable.

import { useComponents } from 'awayto';

function ParentComponent(props: IProps): JSX.Element {

  const { SomeComponent } = useComponents();

  return <SomeComponent {...props} />;

}

useFileStore

useFileStore is used to access various types of pre-determined file stores. All stores allow CRUD operations for user-bound files. Internally default instantiates {@link AWSS3FileStoreStrategy}, but you can also pass a {@link FileStoreStrategies} to useFileStore for other supported stores.

import { useFileStore } from 'awayto';

const files = useFileStore();

const file: File = ....
const fileName: string = '...';

// Make sure the filestore has connected
if (files)
 await files.post(file, fileName)

If you feel that this list doesn’t answer your questions, please join the discord to suggest changes.

What gets installed?

You can review the old installation guide to get an idea of the more detailed changes the installation process applies to AWS. In general, after running the installer, the following resources will be deployed to AWS:

  • Cognito User Pool
  • Cognito App Client
  • RDS Postgres Instance (t3.micro)
  • API Gateway stage containing authorized and unauthorized routes
  • Lambda Function to receive gateway events
  • Service worker ‘LambdaTrust’ role for lambda-service group access
  • S3 buckets for hosting built web and API bundles
  • CloudFormation distribution to serve the webapp from S3
  • Local file system containing a generic Lambda node-based API, Typescript type suite, and React web app originally generated with create-react-app.

What is the seed file?

Awayto intends to be a re-packagable system, so that your creations can be used in various contexts. The seed file collects all the details of your installation. Should you need the ID of a resource, or to know what’s been deployed, you’ll find it in the seed files found in bin/data/seeds.

Where are things located in the file system?

The project is divided into three main code bases:

API: src/api

The API is a generic Node JS structure which implements a Lambda function. The index.ts file collects all the db objects, and generates a set of routes defined by the type system. When called, the events will process inputs and return outputs as described by their associated type. In this way, the standard convention is that all API routes should be directly tied to some type within the system.

Core: src/core

The core is where all the abstractions of Awayto live. Here we store all the types and third-party framework implementations as much as they can be abstracted from either the API or webapp, respectively. I.e. the core should contain only that which could be of use to both the API and webapp, and not their dependent parts. Don’t incur the API to build webapp resources by building the webapp in the core, for example.

Webapp: src/webapp

The webapp is a React application originally generated with create-react-app. React components in Awayto are considered to be the parts that make up a “module”, and should all be stored in a modules folder. As well, you will find a hooks folder containing convenience methods and utilities.

Landing: src/landing

The Hugo framework is used to create a basic, static landing/marketing site. The intention is to create a resource based separation of concerns so in the future our webapp can be treated separately from any marketing or informational site.

How does the module system work?

The src/webapp/modules folder houses, by convention, self-contained packages that live on their own but can interact with other modules if needed. When developing, all the modules are dynamically loaded into memory and lazy loaded into the application. Modules should contain all your React components, Redux reducers, and Typescript types. Components are loaded in a failsafe manner, and so can interact with eachother when present, but will just render an empty div if a module is missing.

How to add new components?

React components should sit in the main level of a module folder. The name of the component file will be the corresponding name you would use to reference it when making use of the useComponents hook.

How to add new types?

Types are used in multiple contexts to accomplish different functionalities in Awayto: basic type-based architecture, Redux structure, and API routes. A basic usage that covers all these cases would look something like this:

// src/core/types/common.ts

declare global {
  interface ISharedState {
    test: ITestState;
  }

  type ICommonModuleActions = ITestActions | ....;

  interface ISharedActionTypes {
    test: ITestActionTypes;
  }
}

export type ITest = {
  id: string;
  value: string;
}

export type ITestState = Partial<ITest>;

export enum ITestActionTypes {
  POST_TEST = "POST/test",
  PUT_TEST = "PUT/test",
  GET_TEST = "GET/test",
  GET_TEST_BY_ID = "GET/test/:id",
  DELETE_TEST = "DELETE/test/:id",
  DISABLE_TEST = "PUT/test/disable"
}

export type IPostTestAction = PayloadAction<ITestActionTypes.POST_UUID_NOTES, ITestState>;
export type IPutTestAction = PayloadAction<ITestActionTypes.PUT_UUID_NOTES, ITestState>;
export type IGetTestAction = PayloadAction<ITestActionTypes.GET_UUID_NOTES, ITestState>;
export type IGetTestByIdAction = PayloadAction<ITestActionTypes.GET_UUID_NOTES_BY_ID, ITestState>;
export type IDeleteTestAction = PayloadAction<ITestActionTypes.DELETE_UUID_NOTES, ITestState>;
export type IDisableTestAction = PayloadAction<ITestActionTypes.DISABLE_UUID_NOTES, ITestState[]>;

export type ITestActions = IPostTestAction
  | IPutTestAction
  | IGetTestAction
  | IGetTestByIdAction
  | IDeleteTestAction
  | IDisableTestAction;

// src/webapp/modules/common/reducers/test.ts

const initialTestState: ITestState = {};

const testReducer: Reducer<ITestState, ITestActions> = (state = initialTestState, action) => {
  switch (action.type) {
    case ITestActionTypes.POST_TEST:
    case ITestActionTypes.PUT_TEST:
    case ITestActionTypes.GET_TEST_DETAILS:
    case ITestActionTypes.GET_TEST_DETAILS_BY_SUB:
    case ITestActionTypes.GET_TEST_DETAILS_BY_ID:
    case ITestActionTypes.DISABLE_TEST:
      return { ...state, ...action.payload };
    default:
      return { ...state };
  }
};

// Extend the useApi hook's available actions in src/webapp/hooks/useApi.ts

let ApiActions = Object.assign(
  ITestActionTypes, // Add our Action Types
  IManageUsersActionTypes,
  IManageGroupsActionTypes,
  IManageRolesActionTypes,
  IUserProfileActionTypes
) as Record<string, string>;

With these structures in place, you would be able to now create API routes that link to those defined, as well as access the object through Redux.

How do I deploy my new database scripts?

Any new .sql files in the src/api/scripts folder can be deployed by running npm run db-update from the main directory.