This document contains only naming conventions, please refer to Code Standards for more code standards.
Also, this document is intended specifically for code, so please take a look at Naming Standards for other naming conventions (for example, npm script names).
Typically a combination of Pascal Case and Camel Case is used in
TypeScript projects. Atoll uses modified versions of both. In
the case where Pascal Case is normally used (for example, classes
and interface names) we allow successive capitalized letters, for
example, "HTML" instead of "Html". An example of this is:
HTMLInputElement
(from the browser "Web API").
- Folder names should use lowercase letters.
- Folder names should use dashes to separate words.
- Folder names should not use underscores to separate words.
- Don't precede interface types with any prefix (for example, "I" for interface or "T" for type, as used in other code standards).
- Use the prefix "Base" for an interface that is at the root of the type hierarchy but typically isn't used directly by objects.
- Use the prefix "Standard" for an interface that is a lowest common denominator for objects that will extend it.
- Avoid deeply nested hierarchies and instead try to combine other interfaces (for example, StandardInvertibleComponentProps)
- Don't use the "Standard" interfaces as replacements for component property types
(for example, AppIconProps is an alias for StandardInvertibleComponentProps so that AppIcon has its own props type)
NOTE: This is done so that consumers of AppIcon aren't aware of StandardInvertibleComponentProps so that they can evolve separately. - Preserve acronym case in interface names (to follow Web API standards, for example, HTMLInputElement).
- Database related objects should be prefixed by
db
, for exampleconst dbProject = await ProjectDataModel.findByPk(projectId);
The components are organized using Atomic Design principles, so the following base folders should be used:
- "atoms" = basic building block components
- "molecules" = when smaller building blocks are combined they form molecules, for example, "backlog item card"
- "organisms" = defining sections of the applicaton, for example, "top menu panel", "backlog item planning panel"
- "templates"
- "pages"
NOTE: Do not assume that components belong in an upper level folder if they contain items at a lower level. In a similar vein, do not assume something belongs at the lower level because it doesn't contain anything from that level. Use the guidelines provided by Atomic Design itself. It is best to think of this from the UI/UX designer's point of view instead of thinking technically how the components are composed. A good example of this is used above: "backlog item card" is a "molecule" but, when this was written, it didn't use any "atoms" - but from a UI/UX perspective it does appear to have many smaller building blocks that could potentially be atoms.
4 Types of Data Objects
- API Data Model (response) - the exact structure received from the Restful API (e.g. dates returned as ISO Date String).
- API Data Model (request) - the exact structure sent to the Restful API (e.g. dates are automatically converted).
- Redux State Model - the structure used for the application state.
- Sequelize Data Model - the structure retrieved from the database.
Model Type | Name Format | Example |
---|---|---|
API Data Model (response) | Api{Entity} | ApiSprint |
API Data Model (request) | {Entity}Model | SprintModel |
Reduxe State Model | {Entity} | Sprint |
Sequelize Data Model | {Entity}DataModel | SprintDataModel |
Why We Need All 4
- Most databases have something that can be a boolean, but it may not specifically be true/false. So, in Atoll we map boolean to char(1) - "Y" or "N" values.
- Redux state (i.e. application state) has specialized needs so it may contain additional fields or not need fields provided by either the API or the data model.
- The API data model may return data types in a way that works best for a Restful API (for example ISO Dates) so they will need to be converted to their equivalent types when stored in the application state.
- The API request gets automatically converted (e.g. date gets converted to a different type).
Technically, one could work with the API Model directly for the redux state but we favor the benefits of abstraction over a little extra coding.
Table names should use only lowercase letters. For example, backlogitempart
.
Field names are camelCased but one exception is for foreign keys. In this case the full table name is used (all lowercase) followed
by "Id". For example, backlogitempartId
.
The action naming falls into a number of categories detailed below:
- Constant naming
- Constant value format
- Flow-related naming
- All uppercase, words separated by underscores, e.g.
INIT_APP
Simple "global" actions: app/{verb}
Targeted actions: app/{target}:{verb}
API actions: app/api:{call}:{stage}
Actions fit into a number of categories:
- Simple global actions
- Targeted actions
- API actions
- Life-cycle
Life-cycle overrides all other naming, so it is defined first.
Life-cycle
UI action prefermed, API call is made, result is succesful, data needs to be retrieved, UI needs to be updated.
- UI action is performed (button clicked etc.)
- API call naming is defined below and follows the specific API call life-cycle:
- Request
- Success / Failure
- Orchestration Middleware then processes the sucessful result and retrieves any other data that will be needed by reducers, for example if the backlog item ID was used for the operation it may be necessary to retrieve the full backlog item data before proceeding.
- UI then needs to be updated - the action will need to make it obvious that just a local state change results from this.
Example:
- MOVE_SPRINT_ITEM_TO_PRODUCT_BACKLOG_CLICK
- API_DELETE_SPRINT_BACKLOG_ITEM_REQUEST
- API_DELETE_SPRINT_BACKLOG_ITEM_SUCCESS
- ADD_PRODUCT_BACKLOG_ITEM
- REMOVE_SPRINT_BACKLOG_ITEM
Simple Global Actions
Example: INIT_APP = "app/init"
Targeted Actions
Example: LOCAL_STORE_REFRESH_TOKEN = "app/local-store:refresh-token"
local-store
is the "target" of the actionrefresh-token
is the verb performed for the "target"
API Actions
Example: API_GET_USER_PREFS_REQUEST: "app/api:get-user-prefs:request"
api
is a prefixed "namespace" to differentiate these actionsget-user-prefs
is the API call descriptorget
is the HTTP verb and can beget
,post
,put
,patch
, ordelete
user-prefs
is the resource targeted
request
is the stage, other values for stage are:success
andfailure
There are two types of selectors:
- Root state selectors
- State slice selectors
Each of these selectors has slightly different rules because of how and where they are typically used.
Selector Type | Name Format | Example |
---|---|---|
Root State | select* | selectBacklogItemById |
State Slice | sliceSelect* | sliceSelectBacklogItemById |
These selectors work with the root state object and can derive their results from multiple state slices that are produced by different reducers. However, most typically they work with a single state slice. These selectors are used wherever the root state is available (redux middleware, thunks, redux containers, etc.)
file: ./selectors/backlogItemSelectors.ts
// selectors
import * as backlogItemsSliceSelectors from "../reducers/backlogItems/backlogItemsSliceSelectors";
...
export const selectBacklogItemById = (state: StateTree, itemId: string): BacklogItem | null =>
backlogItemsSliceSelectors.sliceSelectBacklogItemById(state.backlogItems, itemId);
file: ./middleware/sprintBacklogItemMiddleware.ts
// selectors
import * as backlogItemSelectors from "../selectors/backlogItemSelectors";
...
const backlogItem = backlogItemSelectors.selectBacklogItemById(state, backlogItemId);
if (!backlogItem) {
throw new Error(`Unable to find backlog item with ID ${backlogItemId}`);
}
State slice selectors are used when only a subset of the data is available- one example is the reducer itself. For the same reason we use root state selectors, it is valuable to use state slice selectors that encompass the same logic. Whenever a state slice selector is provided, the equivalent root state selector should use that same state slice selector so that refactoring them is coupled so the changes can be done to both simultaneously.
file: ./reducers/backlogItems/backlogItemsSliceSelectors.ts
export const sliceSelectBacklogItemById = (backlogItems: BacklogItemsState, itemId: string): BacklogItemWithSource | null => {
const matchingItems = backlogItems.allItems.filter((item) => item.id === itemId);
if (matchingItems.length === 1) {
const matchingItem = matchingItems[0];
return matchingItem as BacklogItemWithSource;
} else {
return null;
}
};
file: ./reducers/backlogItems/backlogItemsReducer.ts
// selectors
import * as backlogItemsSliceSelectors from "./backlogItemsSliceSelectors";
...
case ActionTypes.TOGGLE_BACKLOG_ITEM_DETAIL: {
const actionTyped = action as ToggleBacklogItemDetailAction;
draft.openedDetailMenuBacklogItemId = calcToggledOpenMenuItemId(
draft.openedDetailMenuBacklogItemId,
actionTyped.payload.itemId,
strictMode,
(itemId: string) => backlogItemsSliceSelectors.sliceSelectBacklogItemById(state, itemId),
(item) => item.pushState !== PushState.Removed
);
return;
}
Some redux related code standards came from this page: https://redux.js.org/usage/deriving-data-selectors