Giter VIP home page Giter VIP logo

airtable-schema-generator's Introduction

airtable-schema-generator

An NPM package designed to support React + Node apps that use Airtable as a backend!

Table of contents generated with markdown-toc

What it does

After you install and setup the package, running yarn generate-schema (or npm run generate-schema), the script will scrape the Airtable website, download the latest schema for your app, and generate helper files.

The helper files establish abstraction barriers for the rest of your project to follow. At the lowest level is airtable.js, which contains the functions that directly interact with airtable. Next, comes request.js, which are CRUD functions generated for every table in your Airtable base. Finally is schema.js, a source of truth for your airtable schema.

Besides organizing your code's interactions with airtable via helper functions, the package also makes Airtable records easier to work with! The file schema.js contains mappings from Airtable Column Names to a more javascript-y version of them. For example, "First Name" becomes "firstName". It's aware of linked records as well; "Project Group" (which might be a one-to-one linked record column) will become "projectGroupId".

In your codebase, you can use the javascript-y names, and the provided airtable.js file will automatically transform them to and from the actual Airtable column names! See more here

Take a look at schema.js after you generate the files to get a sense of what the record transformations looks like for your Airtable base. Also explore request.js to see what functions you will be interacting with.

Installation

1) Add package as a dev dependency

npm install --save-dev airtable-schema-generator or yarn add -D airtable-schema-generator

2) Choose a mode to use the package in

Options:

  1. Auto: You will provide your username and password in a .airtable.env file and the schema generator will scrape the airtable API page via an automated web browser
  2. Auto (Headless): Same as above, just without a visible web browser
  3. Manual: You manually scrape airtable API page and save to file. No username and password needed

3) Add generator settings to your package.json file

In your package.json add the following:

"airtable-schema-generator": {
  "mode": "<Desired Mode>",
  "output": "path/to/output/folder"
}

The output folder is where you'd like your generated files to live. The mode parameter accepts "auto", "auto-headless", and "manual"

Other Parameters: input: Only required if mode is set to Manual. Similar to output, specifies where schemaRaw.json can be found. Details below defaultView: You can specify a default view to use for Read requests. Default is "Grid View" schemaMeta: Where your metadata for schema generation lives. Details below envFileName: The name of your environment variable file. Default is ".env" exceptions: Specify exceptions for pluralization/depluralization of table names in request.js. Details below airlock: Boolean parameter as to whether you are using @calblueprint/airlock. Default is false overwrite: Boolean parameter as to whether you want to overwrite the local airtable.js file. Default is true. Note: When updating schema generator versions, having this set to false might break the code.

4) Create a .env file

Create a file called .env in the root of your project and fill it in with the following:

REACT_APP_AIRTABLE_BASE_ID=
REACT_APP_AIRTABLE_API_KEY= // Only necessary if not using Airlock
REACT_APP_AIRTABLE_ENDPOINT_URL= // Only necessary if using airlock

## Needed if using Auto mode
AIRTABLE_EMAIL=
AIRTABLE_PASSWORD=

If you already have a .env, you can add these keys to the existing .env file. The Email and Password is only needed if using auto mode. This information is only saved on your computer in this hidden file.

If you'd like to use a different name for your environment file, pass it in in package.json under envFileName

5) Add new env file to gitignore

Add the line .env to your .gitignore file

6) Add convenient run script

Update your scripts object in package.json to include the following

"scripts": {
  "generate-schema": "generate-airtable-schema"
}

Optional, add && pretty-quick or your own prettifier command to the script to post-process the schema code.

Running the script

Run npm run generate-schema to re-generate your utility functions every time your airtable base updates!

CRUD Functions

The functions it generates are below. The names of the functions are based on the table names. Names are naively pluralized by seeing if the last letter is an "s" or not.

getAllRecords: Gets all records of table

getRecordById: Gets a record given an ID

getRecordsByIds: Gets an array of records given an array of IDs

getRecordsByAttribute: Gets an array of records for which a certain attribute has a certain value

createRecord: Creates record given object containing fields to create

updateRecord: Updates record given an ID and object containing fields to update

deleteRecord: Deletes record given an ID

Important Assumptions

This package makes assumptions of how you structure your Airtable. We note them here as best as possible

  1. Linked Record relationship is accurate (one-to-many vs one-to-one)
  2. Column Names do not have any special characters not accounted for (detailed under Record Transformations)
  3. There are no two column names that would map to the same Javascript-y name. For example, "First Name" and "First Name?" would both map to "firstName" and isn't supported

Manual Mode

If you'd prefer not to use one of the two automatic modes, the schema generator will instead look for a file called schemaRaw.json in your specified input folder. In order to generate your schema, do the following:

  1. Navigate to https://airtable.com/<YOUR_BASE_ID>/api/docs
  2. Open the Javascript console (Cmd-Option-J on Chrome for Macs)
  3. Run this command (it will copy the result to your clipboard): copy(_.mapValues(application.tablesById, table => _.set(_.omit(table, ['sampleRows']),'columns',_.map(table.columns, item =>_.set(item, 'foreignTable', _.get(item, 'foreignTable.id'))))));
  4. Paste the result into your schemaRaw.json file
  5. Run yarn generate-schema

You will need to repeat the steps above each time the airtable schema updates and you want to regenerate changes. If you do not update schemaRaw.json, the schema generation will not fail, but rather generate based on an outdated schema.

Note: It's recommended to add schemaRaw.json to your .gitignore file

Schema Metadata

Optionally, you can add a schemaMeta parameter to your airtable-schema-generator entry in package.json. This object lets you specify info about custom helper functions in request.js. Sample Structure:

{
  "User Login": {
    "lookupFields": ["Email"]
  },
  "Announcement": {
    "lookupFields": ["Project Group"]
  }
}

Lookup Fields

The lookupFields meta attribute specifies which fields you would like to create a custom getRecordsByAttribute helper function for. For example, one of the functions the above schemaMeta would create might look like:

export const getAnnouncementsByProjectGroup = async value => {
  return getRecordsByAttribute(
    Tables.Announcement,
    Columns.Announcement.projectGroup,
    value
  );
};

This is in addition to the two default "get" functions created.

NOTE: Prior to version 1.5.1, value was a generic type, which meant that string type names had to be further escaped.

This no longer applies - example usage:

  • getStoresByStoreName("A & S Grocery") --> ({Store Name} = "A & S Grocery")
  • getProductsByPoints(325) --> ({Points} = "325")
  • getStoresByOpen('TRUE()') --> ({Open} = "TRUE()")

Record Transformations

To make working with records easier, this package includes functions that transform Airtable records after a Read action and before a Create/Update action. That transformation consists of the following:

1. Javascript-y Column Names

The transformation changes the column names of the record from the column name on Airtable (usually human readable) to the most likely variable name given basic Javascript conventions.

Examples:

  • "First Name" -> "firstName"
  • "Is Public?" -> "isPublic"

The current definition of the map from Human Readable to Javascript-y names is:

  1. Remove Special Characters: (, ), ``, ", `'`, `?` (more can be added through a PR)
  2. Lowercase Entire String
  3. Split on spaces
  4. Capitalize first character of each word except the first
  5. Combine

2. Accurate Linked Record Column Names

Linked Records on Airtable are usually named something like "Author" or "Project", which would make the corresponding JavaScript-y name "author" or "project". The most expressive name, however, given how they come back in the API response, would be "authorId" or "projectId".

Record transformation accounts this, pluralizing it when the record is marked as a one-to-many relationship.

2.1 Pluralization and depluralization

Because the function names imply whether to use a single record or multiple records, we pluralize and depluralize table names when generating request.js. The implementation of pluralization and depluralization is very simple - we just check if the tablename ends in the letter 's'. This means we don't make any assumptions about whether your Airtable base follows the convention of naming with singular case or plural case!

However, English can be odd - for example, a table named "Feedback" would not need to be pluralized. On the other hand, "News" shouldn't be de-pluralized. Thus, you can specify exceptions in the generator settings in package.json. It should be in this format:

"exceptions": {
      "pluralize": ["Feedback", "Testing"],
      "depluralize": ["News"]
    }

For example, this input would have an output in request.js of:

// pluralize exception:
export const createFeedback ...
export const createManyFeedback ...

// depluralize exception
export const createNews ...
export const createManyNews ...

// no exception
export const createClerk ...
export const createManyClerks ...

We don't support specialized pluralization or depluralization to handle cases such as "Sites Browsed".

3. Wraps/Unwraps one-to-one Linked Record Values

Even though Airtable allows you to change a linked record to a one-to-many relationship, the values from the API are still considered an array, meaning you have to unwrap a value from an array when reading from airtable and re-wrap it when writing

Record transformation does this wrapping and unwrapping for you based on the type of relationship found on Airtable.

Custom Filters and Sorts

For all getAllRecords functions, getRecordsByIds functions, and getAllRecordsByAttribute functions (generated by lookup fields), you can provide optional filterByFormula and sort props that will be passed to the base Airtable functions.

The formula field reference may be helpful. Additionally, you can test these formulas by creating a new Formula type column.

NOTE: sort expects an array, formatted thus: [{field: "Primary Key", direction: "desc"}]. direction defaults to asc.

Example usage:

  const stores = await getStoresBySnapOrEbtAccepted(
    'TRUE()',
    '{Coupon Program Partner} = TRUE()',
    [{ field: 'Store Name', direction: 'desc' }]
  );
  const products = await getProductsByIds(
    ids,
    '{Recently Delivered} = TRUE()',
    [{ field: 'Full Name' }]
  );

Notes

This generator was made originally to support Blueprint projects. Learn more about what we do here: https://calblueprint.org/

Known issues

Schema change errors

In version 1.5.1 and earlier, the generated airtable.js would throw errors upon being unable to properly transform the Airtable call internally. It uses the local copy of schema.js to perform the transformations, which unfortunately caused client applications to break upon schema changes until the local copy of schema.js was updated - i.e the generator was re-run.

List of cases by status:

Cases that break the application:

  • Changing a column name [fixed in 1.5.2]
  • Changing a table name

Cases that do not break the application:

  • Creating a new table [data will not be able to be accessed]
  • Creating a new column [data will not be able to be accessed]

Cases we have not tested:

  • Removing a table
  • Removing a column

The plan for fixes is to updated to console.log a warning instead - HOWEVER, you will still need to run the generator upon schema changes to be able to actually process data for new/modified columns and tables. On the other hand, the generator will no longer throw errors causing apps to stop working.

We are still working on thoroughly testing the rest of the cases and identifying fixes. Please file an issue or feel free to contribute a PR ๐Ÿ™

airtable-schema-generator's People

Contributors

aivantg avatar annieyro avatar romainquellec avatar wangannie avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

airtable-schema-generator's Issues

Pluralize/depluralize exceptions

Add exceptions for pluralize/depluralize special cases, because English is weird (e.g News - depluralizing; Feedback - pluralizing)

Missing lodash

I tried to follow the instructions here https://github.com/aivantg/airtable-schema-generator#manual-mode but am getting _ is not defined.

The mostly updated code is:

// Need to handle the foreign key part
copy(Object.entries(application.tablesById).reduce((p,[k, v]) => ({...p, [k]: {...v, sampleRows: undefined, $$hashKey: undefined, columns: v.columns.map(c => ({...c, $$hashKey: undefined}))}}), {}))

Depluralize bugs

For the foreign-key-many depluralization, there are rare cases when depluralizing creates bugs:

	clerkFeedbacIds: { name: `Clerk Feedback`, type: `foreignKey-many` }
	productsPurchaseIds: { name: `Products Purchased`, type: `foreignKey-many` },

Fixing this would require a depluralize function similar to the pluralize function

Add optional `sort` to `getRecordsByIds`

getRecordsByAttribute and getRecordByIds functions implicitly use their own filterByFormula but can both take in sort parameters. getRecordsByIds currently does not have one.

Enforce returned records having all column properties exist even if empty

A quirk of the Airtable API is that when it returns records, it will only populate fields if they are non-empty, which is pretty annoying.

Ex: Trying to get products by productIds from a store; initializing productIds to an empty array in the fromAirtableFormat might be useful.

This quirk has caused a few small bugs so think it might be worth pre-filling all the fields (since we know what they are from the schema) to be the correct empty type ('', {}, etc).

Add a batch `findRecordsByIds` function

Currently if you have an array of record IDs, you have to make a unique findRecordById call for each one. Can we make a function that takes in an array of IDs and only makes one fcall?

License?

Hey, thanks for this repo. I checked it out and it's awesome.

I have a different use case: I just want to dump everything into SQL files and your repo gives me a lot of the functionality I need. I wanted to check if it's OK to fork and modify your code. I don't think you will want many of my edits, as I want to pull out a lot of the code which I don't need (CRUD helpers)

Is this MIT licensed? Either way, I'll link back to this repo in my readme

Add `createRecords` & `request.js` templates for batch-creating multiple records


function createRecords(table, records) {
  const transformedRecords = records.map((record) => ({
    fields: toAirtableFormat(record, table),
  }));
  return base(table)
    .create(transformedRecords)
    .then((newRecords) => {
      return newRecords.map((newRecord) => newRecord.getId());
    })
    .catch((err) => {
      throw err;
    });
}

Corresponding request.js:

export const createManyStores = async (records) => {
  const createPromises = [];
  const numCalls = Math.ceil(records.length / 10);
  for (let i = 0; i < numCalls; i += 1) {
    const subset = records.slice(i * 10, (i + 1) * 10);
    if (subset.length > 0)
      createPromises.push(createRecords(Tables.Stores, subset));
  }
  return Promise.all(createPromises);
};

*Airtable API only allows batches of up to 10

Selector in Airtable at generation

Hello there !

When using "generate-airtable-schema", the user get an error because the selector doesnt seems to be good. (I guess they changed it).

On "airtable-schema-generator/lib/scrapper.js", l25 the selector should be :

try { await page.click( '#sign-in-form-fields-root > div > label > button[type="submit"]' ); } catch (err) { await page.click( '#sign-in-form-fields-root > div > label > input[type="submit"]' ); }

Or something similar.

Exclude certain tables and columns from the schema

Add exceptions for columns and tables that should not be included in the schema (don't yell when schema doesn't include them).

Not sure if this is really needed - the use case for this is when one developer is updating the schema, or testing a new column addition, but testing won't be allowed since the generator wants to enforce that the generator is run and breaks the app while trying to test unrelated changes, e.g

Error converting Products record from Airtable. Could not find column of name "Original Customer Cost" in local copy of schema.js. Please run the schema generator again to get updates

The fix for now is to run the generator, but this will re-generate the schema with a column we don't want to keep since it'll be updated in a separate PR.

get__ByIds doesnt seems to work

Hello !
Thanks for the work done here.

I'm using get__ById and get__ByIds methods, but it seems the first one is working fine, but not the last one. One workaround was to create a new field at the end of the table called id with a special formula RECORD_ID(), but it seems wrong (Especially with get_byId working)

Any feedback of this ?

My error : error: "INVALID_FILTER_BY_FORMULA", message: "The formula for filtering records is invalid: Unknown field names: id()

Thanks.

Add `updateRecords` & `request.js` templates for batch-updating multiple records

function updateRecords(table, updatedRecords) {
  const transformedRecords = updatedRecords.map((updatedRecord) => ({
    id: updatedRecord.id,
    fields: toAirtableFormat(updatedRecord.fields, table),
  }));
  return base(table)
    .update(transformedRecords)
    .then((records) => {
      return records[0].id;
    })
    .catch((err) => {
      throw err;
    });
}

Corresponding request.js:


export const updateManyStores = async (recordUpdates) => {
  const updatePromises = [];
  const numCalls = Math.ceil(recordUpdates.length / 10);
  for (let i = 0; i < numCalls; i += 1) {
    const subset = recordUpdates.slice(i * 10, (i + 1) * 10);
    if (subset.length > 0)
      updatePromises.push(updateRecords(Tables.Stores, subset));
  }
  return Promise.all(updatePromises);
};

*Airtable only allows updates in batches of maximum size 10

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.