Skip to content

Instantly share code, notes, and snippets.

@jahands
Last active January 8, 2023 17:08
Show Gist options
  • Save jahands/caabc600a1ef3f9aa388db2d9876d32a to your computer and use it in GitHub Desktop.
Save jahands/caabc600a1ef3f9aa388db2d9876d32a to your computer and use it in GitHub Desktop.
Repeat.dev to Notion Dashboard https://repeat.dev
import { Client as NotionClient } from '@notionhq/client';
import formatISO from 'date-fns/formatISO'
import { utcToZonedTime } from 'date-fns-tz'
import { ThrottledQueue, auth } from '@jahands/msc-utils';
let notion: NotionClient;
function setup(env: Repeat.Env) {
if (!notion) {
notion = new NotionClient({
auth: env.variables.NOTION_API_KEY,
});
}
}
export default {
async webhook(request: Request, env: Repeat.Env) {
const unauthed = auth(request, env.variables.WEBHOOK_KEY);
if (unauthed) return unauthed;
setup(env)
await env.unstable.tasks.add({ note: 'syncing repeats' }, { id: 'sync-repeats' })
},
async cron(cron: Repeat.Cron, env: Repeat.Env) {
setup(env)
return syncRecentRepeatsToNotionDashboard(env)
},
async task(task: Repeat.Task, env: Repeat.Env) {
setup(env)
return syncRepeatsToNotionDashboard(env)
}
};
const queue = new ThrottledQueue({ concurrency: 6, interval: 333, limit: 1 });
async function syncRepeatsToNotionDashboard(env: Repeat.Env): Promise<Response | void> {
let response: NotionListResponse
await queue.add(async () => env.tracing
.span('notion', 'db_list')
.tag('database_id', env.variables.NOTION_DB_ID)
.measure(async () => {
response = (await notion.databases.query({
page_size: 1000,
database_id: env.variables.NOTION_DB_ID
})) as NotionListResponse
}))
// String is the Repeat ID
const notionMap = new Map<string, NotionRow | null>()
response.results.forEach((r) =>
notionMap.set(r.properties.RepeatID.select.name, r))
const repeats = await env.unstable.repeats.list() as RepeatResponse[]
for (const repeat of repeats) {
const properties = getPropertiesForRepeat(repeat)
// TODO: Only do API call if anything actually needs changing
if (notionMap.has(repeat.id)) {
// Update row
const updateFn = async () => {
const notionRow = notionMap.get(repeat.id)
await notion.pages.update({
page_id: notionRow.id,
properties,
});
}
queue.add(async () => await env.tracing
.span('notion', 'update_page')
.tag('repeat_name', repeat.name)
.tag('repeat_id', repeat.id)
.tag('notion_id', notionMap.get(repeat.id).id)
.measure(updateFn))
} else {
// Create new row
const createFn = async () => {
notionMap.set(repeat.id, null)
await notion.pages.create({
parent: {
'type': 'database_id',
database_id: env.variables.NOTION_DB_ID,
},
properties,
});
}
queue.add(async () => await env.tracing
.span('notion', 'create_page')
.tag('repeat_name', repeat.name)
.tag('repeat_id', repeat.id)
.measure(createFn))
}
}
await queue.onIdle()
await markDeleted(repeats, notionMap, env)
}
/** Only syncs recently modified repeats */
async function syncRecentRepeatsToNotionDashboard(env: Repeat.Env): Promise<Response | void> {
const allRepeats = await env.unstable.repeats.list() as RepeatResponse[]
const maxAge = 1000 * 60 * 60 // 1 hour
const repeats = allRepeats.filter((r) => Date.now() - new Date(r.updated_at).getTime() < maxAge)
if (repeats.length === 0) {
console.log('Nothing to update! Stopping...')
return
}
let response: NotionListResponse
await queue.add(async () => env.tracing
.span('notion', 'db_list')
.tag('database_id', env.variables.NOTION_DB_ID)
.measure(async () => {
response = (await notion.databases.query({
page_size: 1000,
database_id: env.variables.NOTION_DB_ID
})) as NotionListResponse
}))
// String is the Repeat ID
const notionMap = new Map<string, NotionRow | null>()
response.results.forEach((r) =>
notionMap.set(r.properties.RepeatID.select.name, r))
console.log(`Syncing ${repeats.length} out of ${allRepeats.length}`)
for (const repeat of repeats) {
const properties = getPropertiesForRepeat(repeat)
// TODO: Only do API call if anything actually needs changing
if (notionMap.has(repeat.id)) {
// Update row
const updateFn = async () => {
const notionRow = notionMap.get(repeat.id)
await notion.pages.update({
page_id: notionRow.id,
properties,
});
}
queue.add(async () => await env.tracing
.span('notion', 'update_page')
.tag('repeat_name', repeat.name)
.tag('repeat_id', repeat.id)
.tag('notion_id', notionMap.get(repeat.id).id)
.measure(updateFn))
} else {
// Create new row
const createFn = async () => {
notionMap.set(repeat.id, null)
await notion.pages.create({
parent: {
'type': 'database_id',
database_id: env.variables.NOTION_DB_ID,
},
properties,
});
}
queue.add(async () => await env.tracing
.span('notion', 'create_page')
.tag('repeat_name', repeat.name)
.tag('repeat_id', repeat.id)
.measure(createFn))
}
}
await queue.onIdle()
await markDeleted(allRepeats, notionMap, env)
}
async function markDeleted(repeats: RepeatResponse[], notionMap: Map<string, NotionRow>, env: Repeat.Env): Promise<void> {
const repeatMap = new Map<string, boolean>()
repeats.forEach((r) => repeatMap.set(r.id, true))
// Set deleted Repeats as inactive in Notion
for (const [repeatID, notionRow] of notionMap) {
if (!repeatMap.has(repeatID)) {
console.log(`setting ${repeatID} as deleted`)
// Set it as Deleted
const updateFn = async () => {
await notion.pages.update({
page_id: notionRow.id,
properties: {
Status: {
select: {
name: 'Deleted'
}
}
} as Properties
});
}
queue.add(async () => await env.tracing
.span('notion', 'update_page')
.tag('repeat_status', 'deleted')
.tag('repeat_name', notionMap.get(repeatID).properties.Name.title[0].text.content)
.tag('repeat_id', repeatID)
.tag('notion_id', notionRow.id)
.measure(updateFn))
}
}
await queue.onIdle()
}
function getPropertiesForRepeat(repeat: RepeatResponse): Properties {
const timeZone = 'America/Chicago'
const zonedDate = utcToZonedTime(new Date(repeat.updated_at).getTime(), timeZone)
const dateString = formatISO(zonedDate)
const properties = {
RepeatID: {
select: {
name: repeat.id
}
},
Name: {
title: [
{
text: {
content: repeat.name,
link: { url: getRepeatUrl(repeat.id) }
}
}
]
},
Status: {
select: {
name: 'Active'
}
},
URL: {
url: getRepeatUrl(repeat.id)
},
Updated: {
date: {
start: dateString,
time_zone: timeZone
}
}
} as Properties
return properties
}
function getRepeatUrl(repeatId: string): string {
return `https://dash.repeat.dev/repeats/${repeatId}`
}
/** ========== TYPES ========== */
interface RepeatResponse {
id: string;
name: string;
script: string;
created_at: Date;
updated_at: Date;
repeat_version_id: string;
last_trace: LastTrace;
repeat_events: RepeatEvent[];
}
interface LastTrace {
outcome: Outcome;
stats: Stats;
time: Date;
}
enum Outcome {
Exception = "exception",
Ok = "ok",
}
interface Stats {
logs: number;
errors: number;
duration: number;
warnings: number;
}
interface RepeatEvent {
id: string;
type: RepeatType;
}
enum RepeatType {
Cron = "cron",
Email = "email",
Webhook = "webhook",
}
// ======= Notion List Response ======== //
export interface NotionListResponse {
object: string;
results: NotionRow[];
next_cursor: null;
has_more: boolean;
type: string;
page: Page;
}
export interface Page {
}
export interface NotionRow {
object: string;
id: string;
created_time: Date;
last_edited_time: Date;
created_by: TedBy;
last_edited_by: TedBy;
cover: null;
icon: null;
parent: Parent;
archived: boolean;
properties: Properties;
url: string;
}
export interface TedBy {
object: string;
id: string;
}
export interface Parent {
type: string;
database_id: string;
}
export interface Properties {
URL: URL;
Status: RepeatID;
Updated: Updated;
RepeatID: RepeatID;
Tags: Tags;
Name: Name;
}
export interface Name {
id: string;
type: string;
title: Title[];
}
export interface Title {
type: string;
text: Text;
annotations: Annotations;
plain_text: string;
href: null;
}
export interface Annotations {
bold: boolean;
italic: boolean;
strikethrough: boolean;
underline: boolean;
code: boolean;
color: string;
}
export interface Text {
content: string;
link: Link;
}
export interface Link {
url: string;
}
export interface RepeatID {
id: string;
type: string;
select: Select;
}
export interface Select {
id: string;
name: string;
color: string;
}
export interface Tags {
id: string;
type: string;
multi_select: Select[];
}
export interface URL {
id: string;
type: string;
url: string;
}
export interface Updated {
id: UpdatedID;
type: UpdatedType;
date: DateClass | null;
}
export interface DateClass {
start: Date | string;
end: null;
time_zone: null;
}
export enum UpdatedID {
The5BQzw = "%5BQzw",
}
export enum UpdatedType {
Date = "date",
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment