-
-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fetch airtable and consolidate all customer data together + handle i1…
…8n fields (with fallback value) + automatically resolve relationships
- Loading branch information
1 parent
628f205
commit 5d1274d
Showing
20 changed files
with
407 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { AirtableSystemFields } from './AirtableSystemFields'; | ||
|
||
/** | ||
* Airtable record | ||
* Use generic "fields" field | ||
* | ||
* There are a few differences between the Airtable record format and the one we will return after sanitising it. | ||
* So we force all props in "fields" to be optional to avoid running into TS issues | ||
*/ | ||
export declare type AirtableRecord<Record extends Partial<AirtableSystemFields> = {}> = { | ||
id?: string; | ||
fields?: Partial<Record>; | ||
createdTime?: string; | ||
__typename?: string; // Not available upon fetch, made available after sanitising | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { BaseTable } from '../../utils/api/fetchAirtableTable'; | ||
import { AirtableRecord } from './Airtable'; | ||
|
||
/** | ||
* Dataset containing records split by table | ||
* Used to resolve links (relationships) between records | ||
* | ||
* @example { Customer: Customer[]> , Theme: Theme[]> } | ||
*/ | ||
export declare type AirtableDataset = { | ||
[key in BaseTable]?: AirtableRecord[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { BaseTable } from '../../utils/api/fetchAirtableTable'; | ||
|
||
/** | ||
* Mapping of Airtable fields | ||
* | ||
* Airtable doesn't tell us if a field "products" is supposed to be an instance of "Product" | ||
* This helps dynamically resolving such links (relationships) between records by manually defining which fields should be mapped to which entity | ||
* | ||
* For the sake of simplicity, DEFAULT_FIELDS_MAPPING contains all mappings (singular/plural) | ||
* | ||
* @example { customer: Customer, customers: Customer, products: Product } | ||
*/ | ||
export declare type AirtableFieldsMapping = { | ||
[key: string]: BaseTable; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* Contains Airtable record common fields, known as "System fields". | ||
* | ||
* Those fields are available on any Airtable record. | ||
*/ | ||
export declare type AirtableSystemFields = { | ||
id: string; | ||
createdTime: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,23 @@ | ||
import { GraphCMSSystemFields } from './GraphCMSSystemFields'; | ||
import { AirtableSystemFields } from './AirtableSystemFields'; | ||
|
||
export type AssetThumbnail = { | ||
url: string; | ||
width: number; | ||
height: number; | ||
} | ||
|
||
/** | ||
* An asset is a Airtable "Attachment" field | ||
* | ||
* All fields are managed internally by Airtable and we have no control over them (they're not columns) | ||
*/ | ||
export declare type Asset = { | ||
id?: string; | ||
handle?: string; | ||
fileName?: string; | ||
height?: number | string; | ||
width?: number | string; | ||
url: string; | ||
filename: string; | ||
size?: number; | ||
mimeType?: string; | ||
url?: string; // Field added at runtime by GraphCMS asset's provider - See https://www.filestack.com/ | ||
|
||
// XXX Additional fields that do not exist on the native GraphCMS Asset model, but you can add them and they'll be handled when using GraphCMSAsset, for instance | ||
alt?: string; | ||
classes?: string; | ||
defaultTransformations?: object; | ||
importUrl?: string; | ||
key?: string; | ||
linkTarget?: string; | ||
linkUrl?: string; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
style?: string | object | any; | ||
title?: string; | ||
} & GraphCMSSystemFields; | ||
type?: string; | ||
thumbnails?: { | ||
small?: AssetThumbnail; | ||
large?: AssetThumbnail; | ||
} | ||
} & AirtableSystemFields; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import { GraphCMSSystemFields } from './GraphCMSSystemFields'; | ||
import { AirtableSystemFields } from './AirtableSystemFields'; | ||
|
||
export declare type AssetTransformations = { | ||
id?: string; | ||
height?: number; | ||
width?: number; | ||
} & GraphCMSSystemFields; | ||
} & AirtableSystemFields; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,14 @@ | ||
import { RichText } from '../RichText'; | ||
import { GraphCMSSystemFields } from './GraphCMSSystemFields'; | ||
import { AirtableSystemFields } from './AirtableSystemFields'; | ||
import { Theme } from './Theme'; | ||
|
||
export declare type Customer = { | ||
id?: string; | ||
ref?: string; | ||
label?: string; | ||
labelEN?: string; | ||
labelFR?: string; | ||
theme?: Theme; | ||
terms?: RichText; | ||
} & GraphCMSSystemFields; | ||
termsEN?: RichText; | ||
termsFR?: RichText; | ||
} & AirtableSystemFields; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
import { AirtableSystemFields } from './AirtableSystemFields'; | ||
import { Asset } from './Asset'; | ||
import { GraphCMSSystemFields } from './GraphCMSSystemFields'; | ||
|
||
export declare type Product = { | ||
id?: string; | ||
title?: string; | ||
description?: string; | ||
images?: Asset[]; | ||
price?: number; | ||
} & GraphCMSSystemFields; | ||
} & AirtableSystemFields; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
import { Asset } from './Asset'; | ||
import { GraphCMSSystemFields } from './GraphCMSSystemFields'; | ||
import { AirtableSystemFields } from './AirtableSystemFields'; | ||
|
||
export declare type Theme = { | ||
id?: string; | ||
primaryColor?: string; | ||
logo?: Asset; | ||
} & GraphCMSSystemFields; | ||
} & AirtableSystemFields; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import fetchAirtableTable from './fetchAirtableTable'; | ||
|
||
// TODO "fetch" is not found here - See https://github.com/vercel/next.js/discussions/13678 | ||
// Skipped until resolved | ||
describe.skip(`utils/api/fetchAirtable.ts`, () => { | ||
const results = {}; | ||
describe(`fetchAirtableTable`, () => { | ||
describe(`should fetch correctly`, () => { | ||
test(`when not using any option`, async () => { | ||
expect(await fetchAirtableTable('Customer')).toMatchObject(results); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import deepmerge from 'deepmerge'; | ||
import size from 'lodash.size'; | ||
import { AirtableRecord } from '../../types/data/Airtable'; | ||
import fetchJSON from './fetchJSON'; | ||
|
||
const AT_API_BASE_PATH = 'https://api.airtable.com'; | ||
const AT_API_VERSION = 'v0'; | ||
|
||
export type ApiOptions = { | ||
additionalHeaders?: { [key: string]: string }; | ||
baseId?: string; | ||
maxRecords?: number; | ||
} | ||
|
||
/** | ||
* Response returned by Airtable when fetching a table (list of records) | ||
*/ | ||
export type GenericListApiResponse<Record extends AirtableRecord = AirtableRecord> = { | ||
records: Record[]; | ||
} | ||
|
||
/** | ||
* List of tables available in the AT Base | ||
*/ | ||
export type BaseTable = 'Customer' | 'Product' | 'Theme'; | ||
|
||
const defaultApiOptions: ApiOptions = { | ||
additionalHeaders: { | ||
Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`, | ||
}, | ||
baseId: process.env.AIRTABLE_BASE_ID, | ||
maxRecords: 10000, | ||
}; | ||
|
||
/** | ||
* Fetches Airtable API to retrieve all records within the given table | ||
* Super simple implementation that only takes care of fetching a whole table | ||
* | ||
* Uses NRN own implementation instead of the official Airtable JS API | ||
* - Ours is much smaller (lightweight) vs theirs - See https://bundlephobia.com/[email protected] | ||
* - We only need to perform "table wide reads" and don't need all the extra create/update/delete features | ||
* - Their TS definitions sucks and are out-of-sync, according to other people - See https://github.com/Airtable/airtable.js/issues/34#issuecomment-630632566 | ||
* | ||
* @example TS types will be automatically inferred, you can also alias "records" to a more obvious name | ||
* const { records: customers } = await fetchAirtableTable<GenericListApiResponse<AirtableRecord<Customer>>>('Customer'); | ||
* const { records: products } = await fetchAirtableTable<GenericListApiResponse<AirtableRecord<Product>>>('Product'); | ||
* | ||
* If you prefer to use their official API: | ||
* Alternatively, you can use the official Airtable JS API at https://github.com/airtable/airtable.js/ | ||
* Async/Await example - https://github.com/UnlyEd/airtable-backups-boilerplate/blob/master/src/utils/airtableParser.js | ||
*/ | ||
const fetchAirtableTable: <ListApiResponse extends GenericListApiResponse = GenericListApiResponse>( | ||
table: BaseTable, | ||
options?: ApiOptions, | ||
) => Promise<ListApiResponse> = async (table: BaseTable, options?: ApiOptions) => { | ||
options = deepmerge(defaultApiOptions, options || {}); | ||
const { additionalHeaders, baseId } = options; | ||
const url = `${AT_API_BASE_PATH}/${AT_API_VERSION}/${baseId}/${table}`; | ||
|
||
// eslint-disable-next-line no-console | ||
console.debug(`Fetching airtable API at "${url}" with headers`, additionalHeaders); | ||
const results = await fetchJSON(url, { | ||
headers: additionalHeaders, | ||
}); | ||
|
||
// eslint-disable-next-line no-console | ||
console.debug(`[${table}] ${size(results?.records)} airtable API records fetched`); | ||
|
||
return results; | ||
}; | ||
|
||
export default fetchAirtableTable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import find from 'lodash.find'; | ||
import { AirtableRecord } from '../../types/data/Airtable'; | ||
import { AirtableDataset } from '../../types/data/AirtableDataset'; | ||
import { Customer } from '../../types/data/Customer'; | ||
import { Product } from '../../types/data/Product'; | ||
import { Theme } from '../../types/data/Theme'; | ||
import { sanitizeRecord } from '../data/airtableRecord'; | ||
import fetchAirtableTable, { GenericListApiResponse } from './fetchAirtableTable'; | ||
|
||
/** | ||
* Fetches all Airtable tables and returns a consolidated Customer object with all relations resolved | ||
* | ||
* Relations are only resolved on the main level (to avoid circular dependencies) | ||
*/ | ||
const fetchCustomer = async (preferredLocales: string[]): Promise<Customer> => { | ||
const customerRef = process.env.NEXT_PUBLIC_CUSTOMER_REF; | ||
const { records: airtableCustomers } = await fetchAirtableTable<GenericListApiResponse<AirtableRecord<Customer>>>('Customer'); | ||
const { records: airtableThemes } = await fetchAirtableTable<GenericListApiResponse<AirtableRecord<Theme>>>('Theme'); | ||
const { records: airtableProducts } = await fetchAirtableTable<GenericListApiResponse<AirtableRecord<Product>>>('Product'); | ||
const dataset: AirtableDataset = { | ||
Customer: airtableCustomers, | ||
Theme: airtableThemes, | ||
Product: airtableProducts, | ||
}; | ||
const airtableCustomer = find(airtableCustomers, { fields: { ref: customerRef } }); | ||
|
||
return sanitizeRecord(airtableCustomer, dataset, preferredLocales); | ||
}; | ||
|
||
export default fetchCustomer; |
Oops, something went wrong.