chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
import { listObjects, getObject, listAttributes } from './objects'
import { listRecords, getRecord, createRecord, updateRecord } from './record'
import * as bp from '.botpress'
export default {
listRecords,
getRecord,
createRecord,
updateRecord,
listObjects,
getObject,
listAttributes,
} as const satisfies bp.IntegrationProps['actions']
+53
View File
@@ -0,0 +1,53 @@
//
import { RuntimeError } from '@botpress/client'
import { AttioApiClient } from '../attio-api'
import * as bp from '.botpress'
export const listObjects: bp.IntegrationProps['actions']['listObjects'] = async (props) => {
const { ctx } = props
const accessToken = ctx.configuration.accessToken
const attioApiClient = new AttioApiClient(accessToken)
try {
const data = await attioApiClient.listObjects()
return { data: data.data }
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new RuntimeError(error.message)
}
}
export const getObject: bp.IntegrationProps['actions']['getObject'] = async (props) => {
const { ctx, input } = props
const accessToken = ctx.configuration.accessToken
const attioApiClient = new AttioApiClient(accessToken)
const { object } = input
try {
const data = await attioApiClient.getObject(object)
return { data: data.data }
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new RuntimeError(error.message)
}
}
export const listAttributes: bp.IntegrationProps['actions']['listAttributes'] = async (props) => {
const { ctx, input } = props
const accessToken = ctx.configuration.accessToken
const attioApiClient = new AttioApiClient(accessToken)
const { object } = input
try {
const data = await attioApiClient.listAttributes(object)
return { data: data.data }
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new RuntimeError(error.message)
}
}
+185
View File
@@ -0,0 +1,185 @@
//
import { RuntimeError } from '@botpress/client'
import { AttioApiClient, Attribute } from '../attio-api'
import * as bp from '.botpress'
type AttioRecordIdentifier = {
workspace_id: string
object_id: string
record_id: string
}
type AttioRecord = {
id: AttioRecordIdentifier
created_at: string
web_url: string
values: Record<string, unknown>
}
function _buildAttributeMaps(attributes: Attribute[]) {
const idToSlug: Record<string, string> = {}
const slugToOptionLabelById: Record<string, Record<string, string>> = {}
for (const attr of attributes) {
// Handle new id structure (object with attribute_id) or fallback to old string id
const id = typeof attr.id === 'object' ? attr.id?.attribute_id : String(attr.id ?? '')
const slug = String(attr.slug ?? attr.api_slug ?? id)
if (id) idToSlug[id] = slug
if (Array.isArray(attr.options) && attr.options.length > 0) {
const map: Record<string, string> = {}
for (const opt of attr.options) {
const oid = String(opt.id ?? '')
if (!oid) continue
const label = String(opt.label ?? opt.name ?? opt.value ?? opt.title ?? opt.slug ?? oid)
map[oid] = label
}
slugToOptionLabelById[slug] = map
}
}
return { idToSlug, slugToOptionLabelById }
}
function _mapValueUsingOptions(value: unknown, optionMap: Record<string, string>) {
const mapOne = (v: unknown) => {
if (v && typeof v === 'object' && !Array.isArray(v)) {
const candidateId = (v as { id?: string }).id
if (candidateId && optionMap[candidateId]) return optionMap[candidateId]
return v
}
if (typeof v === 'string' && optionMap[v]) return optionMap[v]
return v
}
if (Array.isArray(value)) return value.map((v) => mapOne(v))
return mapOne(value)
}
function _humanizeRecordValues(
record: AttioRecord,
idToSlug: Record<string, string>,
slugToOptionLabelById: Record<string, Record<string, string>>
) {
const src: Record<string, unknown> = record?.values ?? {}
const dst: Record<string, unknown> = {}
for (const key of Object.keys(src)) {
const slug = idToSlug[key] ?? key
const optionMap = slugToOptionLabelById[slug]
const raw = src[key]
const value = optionMap ? _mapValueUsingOptions(raw, optionMap) : raw
dst[slug] = value
}
return { ...record, values: dst }
}
function _mapValues(values: { attribute: string; value: string }[]) {
return values.reduce((acc: Record<string, any>, curr) => {
acc[curr.attribute] = curr.value
return acc
}, {})
}
export const listRecords: bp.IntegrationProps['actions']['listRecords'] = async (props) => {
const { ctx, input } = props
const accessToken = ctx.configuration.accessToken
const attioApiClient = new AttioApiClient(accessToken)
const { object, filter, sorts, limit, offset } = input
const filterMap = filter ? _mapValues(filter) : undefined
try {
const data = await attioApiClient.listRecords(object, { filter: filterMap, sorts, limit, offset })
const attributes = await attioApiClient.listAttributes(object)
const { idToSlug, slugToOptionLabelById } = _buildAttributeMaps(attributes.data)
const humanized = data.data.map((rec) => _humanizeRecordValues(rec, idToSlug, slugToOptionLabelById))
return { data: humanized }
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new RuntimeError(error.message)
}
}
export const getRecord: bp.IntegrationProps['actions']['getRecord'] = async (props) => {
const { ctx, input } = props
const accessToken = ctx.configuration.accessToken
const attioApiClient = new AttioApiClient(accessToken)
const { object, id } = input
try {
const data = await attioApiClient.getRecord(object, id.record_id)
const attributes = await attioApiClient.listAttributes(object)
const { idToSlug, slugToOptionLabelById } = _buildAttributeMaps(attributes.data)
const humanized = _humanizeRecordValues(data.data, idToSlug, slugToOptionLabelById)
return { data: humanized }
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new RuntimeError(error.message)
}
}
export const createRecord: bp.IntegrationProps['actions']['createRecord'] = async (props) => {
const { ctx, input } = props
const accessToken = ctx.configuration.accessToken
const attioApiClient = new AttioApiClient(accessToken)
const { object, data } = input
const { values } = data
try {
const valuesMap = _mapValues(values)
const data = await attioApiClient.createRecord(object, { data: { values: valuesMap } })
return { data: data.data }
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new RuntimeError(error.message)
}
}
export const updateRecord: bp.IntegrationProps['actions']['updateRecord'] = async (props) => {
const { ctx, input } = props
const accessToken = ctx.configuration.accessToken
const attioApiClient = new AttioApiClient(accessToken)
const { object, id, data } = input
const { values } = data
try {
const valuesMap = _mapValues(values)
const data = await attioApiClient.updateRecord(object, id.record_id, { data: { values: valuesMap } })
return { data: data.data }
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new RuntimeError(error.message)
}
}
export const listAttributes: bp.IntegrationProps['actions']['listAttributes'] = async (props) => {
const { ctx, input } = props
const accessToken = ctx.configuration.accessToken
const attioApiClient = new AttioApiClient(accessToken)
const { object } = input
try {
const data = await attioApiClient.listAttributes(object)
return { data: data.data }
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new RuntimeError(error.message)
}
}
+212
View File
@@ -0,0 +1,212 @@
// Types for API responses and requests
export type RecordIdentifier = {
workspace_id: string
object_id: string
record_id: string
}
export type AttioRecord = {
id: RecordIdentifier
created_at: string
web_url: string
values: Record<string, unknown>
}
export type AttioObject = {
id?: {
workspace_id: string
object_id: string
}
api_slug?: string
singular_noun?: string
plural_noun?: string
created_at?: string
}
export type Attribute = {
id?: {
workspace_id: string
object_id: string
attribute_id: string
}
title?: string
description?: string | null
api_slug?: string
type?: string
slug?: string
options?: Array<{
id?: string
label?: string
name?: string
value?: string
title?: string
slug?: string
}>
}
export type Sort = {
direction: 'asc' | 'desc'
attribute: string
field: string
}
export type ListRecordsParams = {
filter?: Record<string, string>
sorts?: Sort[]
limit?: number
offset?: number
}
export type CreateRecordData = {
data: {
values: Record<string, unknown>
}
}
export type UpdateRecordData = {
data: {
values: Record<string, unknown>
}
}
export type ApiResponse<T> = {
data: T
}
export type WebhookData = {
event_type: string
filter: Record<string, string> | null
}
export type WebhookCreateData = {
data: {
target_url: string
subscriptions: WebhookData[]
}
}
export type WebhookId = {
workspace_id: string
webhook_id: string
}
export type WebhookResponse = {
id: WebhookId
target_url: string
subscriptions: WebhookData[]
created_at: string
secret: string
}
export type ListRecordsResponse = ApiResponse<AttioRecord[]>
export type GetRecordResponse = ApiResponse<AttioRecord>
export type CreateRecordResponse = ApiResponse<AttioRecord>
export type UpdateRecordResponse = ApiResponse<AttioRecord>
export type ListObjectsResponse = ApiResponse<AttioObject[]>
export type GetObjectResponse = ApiResponse<AttioObject>
export type ListAttributesResponse = ApiResponse<Attribute[]>
export type CreateWebhookResponse = ApiResponse<WebhookResponse>
export class AttioApiClient {
private _accessToken: string
private _baseUrl: string = 'https://api.attio.com/v2'
public constructor(accessToken: string) {
this._accessToken = accessToken
}
private async _makeRequest<T>(
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
endpoint: string,
body?: unknown
): Promise<T> {
const url = `${this._baseUrl}${endpoint}`
const headers: Record<string, string> = {
Authorization: `Bearer ${this._accessToken}`,
'Content-Type': 'application/json',
}
const config: RequestInit = {
method,
headers,
}
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
config.body = JSON.stringify(body)
}
const response = await fetch(url, config)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Attio API Error: ${response.status} ${response.statusText} - ${errorText}`)
}
const data = await response.json()
return data
}
// Records API methods
public async listRecords(object: string, params?: ListRecordsParams): Promise<ListRecordsResponse> {
const endpoint = `/objects/${object}/records/query`
const body = {
filter: params?.filter,
sorts: params?.sorts,
limit: params?.limit,
offset: params?.offset,
}
return this._makeRequest<ListRecordsResponse>('POST', endpoint, body)
}
public async getRecord(object: string, recordId: string): Promise<GetRecordResponse> {
const endpoint = `/objects/${object}/records/${recordId}`
return this._makeRequest<GetRecordResponse>('GET', endpoint)
}
public async createRecord(object: string, data: CreateRecordData): Promise<CreateRecordResponse> {
const endpoint = `/objects/${object}/records`
return this._makeRequest<CreateRecordResponse>('POST', endpoint, data)
}
public async updateRecord(object: string, recordId: string, data: UpdateRecordData): Promise<UpdateRecordResponse> {
const endpoint = `/objects/${object}/records/${recordId}`
return this._makeRequest<UpdateRecordResponse>('PUT', endpoint, data)
}
// Objects API methods
public async listObjects(): Promise<ListObjectsResponse> {
const endpoint = '/objects'
return this._makeRequest<ListObjectsResponse>('GET', endpoint)
}
public async getObject(object: string): Promise<GetObjectResponse> {
const endpoint = `/objects/${object}`
return this._makeRequest<GetObjectResponse>('GET', endpoint)
}
// Attributes API methods
public async listAttributes(object: string): Promise<ListAttributesResponse> {
const endpoint = `/objects/${object}/attributes`
return this._makeRequest<ListAttributesResponse>('GET', endpoint)
}
// Webhook API methods
public async createWebhook(data: WebhookCreateData): Promise<CreateWebhookResponse> {
const endpoint = '/webhooks'
return this._makeRequest<CreateWebhookResponse>('POST', endpoint, data)
}
public async deleteWebhook(webhookId: string): Promise<void> {
const endpoint = `/webhooks/${webhookId}`
await this._makeRequest<void>('DELETE', endpoint)
}
// Utility method to test connection
public async testConnection(): Promise<void> {
await this._makeRequest<{ data: unknown }>('GET', '/self')
}
}
+63
View File
@@ -0,0 +1,63 @@
import { z } from '@botpress/sdk'
import { recordEventSchema } from 'src/schemas'
import * as bp from '.botpress'
export const recordCreated = async ({
payload,
client,
logger,
}: {
payload: z.infer<typeof recordEventSchema>
client: bp.Client
logger: bp.Logger
}) => {
await client.createEvent({
type: 'recordCreated',
payload: {
...payload,
event_type: 'record.created',
},
})
logger.forBot().info(`Record created: ${payload.id.record_id}`)
}
export const recordUpdated = async ({
payload,
client,
logger,
}: {
payload: z.infer<typeof recordEventSchema>
client: bp.Client
logger: bp.Logger
}) => {
await client.createEvent({
type: 'recordUpdated',
payload: {
...payload,
event_type: 'record.updated',
},
})
logger.forBot().info(`Record updated: ${payload.id.record_id}`)
}
export const recordDeleted = async ({
payload,
client,
logger,
}: {
payload: z.infer<typeof recordEventSchema>
client: bp.Client
logger: bp.Logger
}) => {
await client.createEvent({
type: 'recordDeleted',
payload: {
...payload,
event_type: 'record.deleted',
},
})
logger.forBot().info(`Record deleted: ${payload.id.record_id}`)
}
+11
View File
@@ -0,0 +1,11 @@
import actions from './actions'
import { register, unregister, handler } from './setup'
import * as bp from '.botpress'
export default new bp.Integration({
register,
unregister,
actions,
channels: {},
handler,
})
+16
View File
@@ -0,0 +1,16 @@
import { z } from '@botpress/sdk'
import { baseIdentifierSchema } from '../definitions/common'
export const recordEventSchema = z.object({
event_type: z.string(),
id: baseIdentifierSchema.extend({ record_id: z.string() }),
actor: z.object({
type: z.string(),
id: z.string(),
}),
})
export const webhookPayloadSchema = z.object({
webhook_id: z.string(),
events: z.array(recordEventSchema),
})
+61
View File
@@ -0,0 +1,61 @@
import { webhookPayloadSchema } from 'src/schemas'
import { recordCreated, recordUpdated, recordDeleted } from '../events'
import * as bp from '.botpress'
export function safeJsonParse(x: any) {
try {
return { data: JSON.parse(x), success: true }
} catch {
return { data: x, success: false }
}
}
export const handler: bp.IntegrationProps['handler'] = async ({ req, logger, client }) => {
if (!req.body) {
const errorMsg = 'Webhook processing failed: empty body'
logger.forBot().error(errorMsg)
return {
status: 400,
body: JSON.stringify({ error: errorMsg }),
}
}
// Parse and validate the webhook payload
const webhookData = safeJsonParse(req.body)
if (!webhookData.success) {
const errorMsg = 'Webhook processing failed: invalid JSON'
logger.forBot().error(errorMsg)
return { status: 400, body: JSON.stringify({ error: errorMsg }) }
}
const parseResult = webhookPayloadSchema.safeParse(webhookData.data)
if (!parseResult.success) {
const errorMsg = `Webhook processing failed: ${parseResult.error.message}`
logger.forBot().error(errorMsg)
return { status: 400, body: JSON.stringify({ error: errorMsg }) }
}
const { webhook_id, events } = parseResult.data
logger.forBot().info(`Processing webhook ${webhook_id} with ${events.length} events`)
// Process each event
for (const attioEvent of events) {
const event = attioEvent.event_type
switch (event) {
case 'record.created':
await recordCreated({ payload: attioEvent, client, logger })
break
case 'record.updated':
await recordUpdated({ payload: attioEvent, client, logger })
break
case 'record.deleted':
await recordDeleted({ payload: attioEvent, client, logger })
break
default:
logger.forBot().warn(`Unsupported event type: ${event}`)
}
}
return { status: 200 }
}
+2
View File
@@ -0,0 +1,2 @@
export { register, unregister } from './setup'
export { handler } from './handler'
+76
View File
@@ -0,0 +1,76 @@
import * as sdk from '@botpress/sdk'
import { AttioApiClient } from '../attio-api'
import * as bp from '.botpress'
export const register: bp.IntegrationProps['register'] = async ({ ctx, client, webhookUrl, logger }) => {
try {
const accessToken = ctx.configuration.accessToken
const attioClient = new AttioApiClient(accessToken)
// Test the connection using the API client
logger.forBot().info('Testing connection to Attio...')
await attioClient.testConnection()
logger.forBot().info('Connection to Attio successful')
// Check if webhooks already exist
logger.forBot().info('Checking if webhooks already exist...')
const { state } = await client.getOrSetState({
name: 'attioIntegrationInfo',
type: 'integration',
id: ctx.integrationId,
payload: {
attioWebhookId: '',
},
})
if (!state.payload.attioWebhookId) {
logger.forBot().info('Webhooks do not exist. Creating webhooks...')
const webhookResp = await attioClient.createWebhook({
data: {
target_url: webhookUrl,
subscriptions: [
{ event_type: 'record.created', filter: null },
{ event_type: 'record.updated', filter: null },
{ event_type: 'record.deleted', filter: null },
],
},
})
const attioWebhookId = String(webhookResp.data.id.webhook_id)
await client.setState({
type: 'integration',
name: 'attioIntegrationInfo',
id: ctx.integrationId,
payload: { attioWebhookId },
})
}
logger.forBot().info('Webhooks created')
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new sdk.RuntimeError(error.message)
}
}
export const unregister: bp.IntegrationProps['unregister'] = async ({ ctx, client, logger }) => {
try {
const accessToken = ctx.configuration.accessToken
const attioClient = new AttioApiClient(accessToken)
const stateAttioIntegrationInfo = await client.getState({
id: ctx.integrationId,
name: 'attioIntegrationInfo',
type: 'integration',
})
const { state } = stateAttioIntegrationInfo
const { attioWebhookId } = state.payload
if (attioWebhookId) {
await attioClient.deleteWebhook(attioWebhookId)
logger.forBot().info('Webhook successfully deleted')
}
} catch (thrown) {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new sdk.RuntimeError(error.message)
}
}