Skip to main content

Ensuring model association shapes throughout the application stack

· 7 min read

Intro to Json Shapes​

We will consider simple

GET /identities/user/:id​

For a simple endpoint like getting a single Identities::User, we would expect the response to be:

Response:

{
id: ..
country_id: ..
email: ..
first_name: ..
last_name: ..
phone_code: ..
mobile: ..
}

GET /org/user-profiles/:id​

We explore the difference cases of considering associations when getting a single Org::UserProfile.

Response if we do not join tables in the backend:

  • simply rendering the attributes on the model Org::UserProfile
{
id: ...
title: ...
}

Response if I join Identities::User in the backend:

  • Org::UserProfile belongs_to Identities::User
{
id: ..
user: {
id: ..
first_name: ..
last_name: ..
...
}
}

Response if I join all associations of Org::UserProfile in the backend:

  • Org::UserProfile belongs_to Identities::User
  • Org::UserProfile belongs_to Company
{
id: ..
user: {
id: ...
first_name:
last_name:
...
},
company: {
id: ...
}
}

Bad response:

This is a badly designed response because there is no database backed model that follows the following format.

{
id: Org::UserProfile.id,
first_name: Identities::User.first_name,
last_name: Identities::User.last_name,
...
}

The idea is to maintain the json "shape" to follow the backend models.


Problem Statement​

For every careers_job_application, I want to know who posted the careers_job

If I add another relationship to careers_job_applications, do I need to join all the tables?

Since AuthProvider wraps the entire frontend application

  • AuthProvider stores Identities::User and Careers::UserProfile and Org::UserProfile (depending where the user logs into)
  • Every page will know who is calling our backend API
  • This means that the backend doesn't need to join the identities user

However, this only applies for only Career::UserProfile cases.

For an Org::UserProfile, they would need to know names of Careers::UserProfile

  • For example, for each Careers::JobApplication, we would want to display the person name, which lives in Identities::User

GET /org-user/careers/job_applications​

  • As a org user, I want to see who applied for my job

Looking at the relationship:

The person making this call is an org-user, I need to join the tables to display the name of the user who applied.

I would return the following response:

{
careers_job_applications: [{
id:,
//.. attributes
career_user_profile: {
id: ..,
identities_user: {
id: ..
first_name: ..
last_name: ...
}
}
}]
}

GET /org-user/careers/job_applications/:id (#show)​

When showing a single job application:

{
careers_job_applications: {
id:,
//.. attributes
career_user_profile: {
id: ..,
certificates: [{ .. }],
experiences: [{ .. }],
skills: [{ .. }],
identities_user: {
id: ..
first_name: ..
last_name: ..
}
}
}
}

How can we reduce joins to improve performance?​

Add a json column career_user_profiles.data, every time we save a Career::UserSomething (i.e. skills, experiences, certificates), we update two tables:

  • Career::UserSomething table
  • Career::UserProfile.data columns
{
careers_job_applications: [{
id:,
//.. attributes
career_user_profile: {
id: ..,
data: { // new column that pre-computes the "join" of the associated tables
certificates: { .. },
experience: { .. },
}
identities_user: {
id: ..
first_name: ..
last_name: ...
}
}
}]
}

The downside of this is that we require to update the tables whenever any one of the Career::UserProfile associations change.

  • then again, it's fairly simple to ensure this by ensuring we call a, for example, Careers::UserProfiles::UpdateDataService in all the places that updates Career::UserProfile associations.

Charlie Question

Imagine we are in a company backend and frontend are two separate teams.

  • I can see that response will always follow the convention where the payload will be correctly formatted (i.e. following the model "shape")
  • can frontend user ignore the shape when they are submitting the form?
    • I imagine frontend does not have visibility how it is implement in the backend

Backend creates endpoint:

  • Send the postman doc to Frontend
  • Frontend would see it and implement their designs
  • Frontend realise in a particular page UI design, the endpoint provided does not return data from a specific model
  • Backend say i cannot join tables any more cause the response you got initially is already joining 10 tables
  • Backend will say, you call this other endpoint to get that model

So what have we established?​

  • The default API Response JSON "shape" should follow the model associations.
  • Ideally max up to 3 table joins in the backend

How does this "model association json shape" affect forms in the frontend?​

Forms should always follow the model it is rendering for.

If want to create a Car, I would have a CarForm that follows the Car attributes.

  • one form field for each Car attribute.

Likewise, if want to create a Org::UserProfile, I would have a OrgUserProfileForm that follows the Org::UserProfile attributes.

Org::UserProfile Attributes

  • id
  • identities_user (association)
  • company (association)
  • title
  • created_at
  • updated_at

In my <OrgUserProfileForm>, I would need fields for the associations as well.

Scenario 1: Admin​

Assume Admin (jod staff) is creating an Org::UserProfile.

  • Admin can access everything in the system
    • they can see all identities_users and companies
<Form>
<TextInputField name={'title'}>
<IdentitiesUserSelectField name={'identities_user.id'}>
<CompanySelectField name={'company'}>
</Form>
const IdentitiesUserSelectField = () => {
// makes an api call to GET /admin-user/identities/users
// gets a list of identities
// render the list of identities in a dropdown
// user select 1 identity from the response
// update the form OrgUserProfileForm state via react-hook-form
}

When I select a Identities::User and Company from their respective select fields

  • my form state now has the Identities::User attributes and company attributes.
  • this means I can nicely render the models int he frontend after the user selects.

form-state-with-ui.drawio.svg

Scenario 2: Org User​

Assume Org User is creating an Org::UserProfile.

  • Org User can only access everything associated with their company
  • They can only see Identities::User and a single Company, which is the company they belong to.

Thinking about the form schema for OrgUserProfileForm

note

In the frontend, form is named as {ModelName}Form

const orgUserProfileSchema = () => {
title: zod.string()
// company_id: zod.integer()
company: {
id: zod.integer().required()
}
identities_user: {
id: zod.integer()
}
}

const OrgUserProfileForm = () => {
return (
<Form>
<TextInputField name={'title'}/>
<TextInputField name={'identities_user.first_name}/>
<Form/>
)
}

Yuri Question

Job Application <-- we need to insert this
- User Career Profiles <-- we don't need to insert anything in this one
- User Career Experience <-- we need to insert this
note

While the question does not make sense in our domain, consider the same associations in the models.

I want to create a Careers::JobApplication

  • and at the same time create Career::UserExperience

Looking at the model associations (not the association on the database level)

TL;DR: Yes we follow the format.

// form schema follow "shape"
const jobApplicationSchema = {
careers_job: {
id: 1,
},
careers_user_profile: {
id: 1 // we need to know who to create the Careers::UserExperience for
careers_experience: [{
job_title: ..
}]
},
}

// Alternative form schema which does not consider "model association json-shape"
// While it's not wrong from a technical perspective, this is not our convention.
const jobApplicationSchema = {
careers_job_id:
careers_user_profile_id:
careers_experiences: [{ .. }, ...]
}

Imam Highlight:

Whenever you change any form schema/state shape, ensure you are assigning errors to the correct form field names.

  • this will ensure that errors are rendered on the correct field in the form

Naming things​


July Direction​

Week 1 we will make changes to existing MVP based on the technical feedback from ali

  • naming
  • api layer should not have async
  • form schema should follow endpoint structure/shape
  • endpoint should follow shape of model associations

Week 2

  • start on the peripheral services
    • send email
    • simple notification system
    • event/analytics
    • file upload

Week 3

  • continue with peripherals
  • start on product feedback

Week 4

  • continue on product feedback