This technical documentation covers the implementation details of the simple integration made as a part of the interviewing process with Epicbrief company. The goal of this integration is to connect Firebase with another service such as Hubspot in a stable and extensible manner.
To setup the project, follow these steps:
- Clone the repository to your local machine.
- Copy the file .env.example and create a new file .env.
- Fill in the required variables in the .env file. If you need the file I am using, please feel free to ask me for it.
- Run
npm install
to install the dependencies. - Start the project by running
npm run dev
and enjoy the real-time sync!
-
Code Consistency and Formatting: ESLint, Lintstaged, and Prettier are used to ensure that the code is consistently formatted and well-structured. Husky is configured to run the
npm run test
each time a new commit is made, and to lint everything before the code is pushed to the remote branch. -
Secure Push to Open Source Repo: The
.env
file keeps important secrets away from the code, so the project can be securely pushed to an open source repo. The env variables can also be set using Docker or any CI system, making the app compatible with these systems. -
Conventional Commits: All commits follow conventional commits, which are in the format of
"[feat|chore|fix|...]([scope]): [message]"
. This makes it easier and more reliable to read the commits, especially in production-ready projects. -
Reusable Functions and Utils: The project is made up of reusable functions and utilities that can be used in production. For example,
getCollection()
is a utility that creates a connection with Firebase, handles type-safing, acts as an ORM, and keeps the data synced with Firebase if the sync flag is set to true. getCollection was used to create models for deals and meetings as described in the task description.
Heart of the project is located in the /src
folder and it consists of the following subfolders:
-
/services
: This folder contains the API implementation for working with different services, including firebase and hubspot. The architecture is designed to follow the dependency inversion principle and ensures the project's stability even if the service is changed or if we upgrade to a newer API version. The API implementation can be easily configured, while keeping the internal interface the same. -
/models
: This folder contains the main database in firebase, where the models are the collections in firebase. Each model is responsible for performing CRUD operations on the corresponding collection in firebase. AgetCollection()
utility function is provided to make it easy to connect to firebase collections by simply providing a string value, which is the collection name. -
/integrations
: ThecreateIntegration()
utility function provides a unified strategy for creating two-way sync between firebase and a service. Due to the various ways of working with different services, a general-purpose interface is provided that receives functions defining how to create, update, delete, or fetch data from the service. The filessrc/integrations/hubspot/deals.ts
andsrc/integrations/hubspot/meetings.ts
contain all the necessary implementations for the task description and they are using thecreateIntegration()
utility function.
Here's a sample code for creating a new integration:
createIntegration({
model: modelObject,
service: {
name: 'service/scope',
fetch: async () => {
// How can we fetch data from the service?
},
create: async (data) => {
// How can we create a new item in the service?
},
delete: async (id) => {
// How can we delete an item from the service?
}
update: async (id, data) => {
// How can we update an item in the service?
},
},
});
Using this method it would be also easy to define services a separated files or package and use them in createIntegration
. As I mentioned above, the code architecture is designed to extend and upgrade easily.
Due to the importance of the createIntegration()
utility function, it is worth mentioning it in more detail:
The createIntegration()
function is responsible for creating a two-way sync between firebase and a service (in this case, Hubspot). When you call hubspotDeals.sync()
, it performs the following steps to ensure the data is in sync:
- Fetches the data from both firebase and the service.
- Fetches all the related integration items from a middle-collection called
integrations
, which keeps track of the connections between items in firebase and items in the service. - Then it decides what to do based on the following cases:
- If there is no integration item for an item in firebase, the function creates an identical item in the service and creates an integration item to remember the syncing.
- If there is no integration item for an item in the service, the function creates an identical item in firebase and creates an integration item to remember the syncing.
- If the
_updatedAt
in the connection item (from integrations collection) is newer than both of firebase and the service, then it means the data is already in sync and no further action is needed. - If the
_updatedAt
in one or two items are newer than the integration itself, it means that one of the items has been updated recently. The function then compares both items and updates the outdated one, as well as the integration item. - If there is an integration but one of the items doesn't exist, it means that the other has been removed. In this case, the function removes the existing object and its related integration item.