chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,90 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const airtableSelectors = {
'airtable.bases': {
key: 'airtable.bases',
contracts: [selectorContracts.airtableBasesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'airtable.bases',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'airtable.bases')
const data = await requestJson(selectorContracts.airtableBasesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.bases || []).map((base) => ({
id: base.id,
label: base.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'airtable.bases')
const data = await requestJson(selectorContracts.airtableBasesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
baseId: detailId,
},
signal,
})
const base = (data.bases || []).find((b) => b.id === detailId) ?? null
if (!base) return null
return { id: base.id, label: base.name }
},
},
'airtable.tables': {
key: 'airtable.tables',
contracts: [selectorContracts.airtableTablesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'airtable.tables',
context.oauthCredential ?? 'none',
context.baseId ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.baseId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'airtable.tables')
if (!context.baseId) {
throw new Error('Missing base ID for airtable.tables selector')
}
const data = await requestJson(selectorContracts.airtableTablesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
baseId: context.baseId,
},
signal,
})
return (data.tables || []).map((table) => ({
id: table.id,
label: table.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'airtable.tables')
if (!context.baseId) return null
const data = await requestJson(selectorContracts.airtableTablesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
baseId: context.baseId,
},
signal,
})
const table = (data.tables || []).find((t) => t.id === detailId) ?? null
if (!table) return null
return { id: table.id, label: table.name }
},
},
} satisfies Record<Extract<SelectorKey, 'airtable.bases' | 'airtable.tables'>, SelectorDefinition>
@@ -0,0 +1,37 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const asanaSelectors = {
'asana.workspaces': {
key: 'asana.workspaces',
contracts: [selectorContracts.asanaWorkspacesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'asana.workspaces',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'asana.workspaces')
const data = await requestJson(selectorContracts.asanaWorkspacesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.workspaces || []).map((ws) => ({ id: ws.id, label: ws.name }))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'asana.workspaces')
const data = await requestJson(selectorContracts.asanaWorkspacesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const ws = (data.workspaces || []).find((w) => w.id === detailId) ?? null
if (!ws) return null
return { id: ws.id, label: ws.name }
},
},
} satisfies Record<Extract<SelectorKey, 'asana.workspaces'>, SelectorDefinition>
@@ -0,0 +1,73 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const attioSelectors = {
'attio.objects': {
key: 'attio.objects',
contracts: [selectorContracts.attioObjectsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'attio.objects',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'attio.objects')
const data = await requestJson(selectorContracts.attioObjectsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.objects || []).map((obj) => ({
id: obj.id,
label: obj.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'attio.objects')
const data = await requestJson(selectorContracts.attioObjectsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const obj = (data.objects || []).find((o) => o.id === detailId) ?? null
if (!obj) return null
return { id: obj.id, label: obj.name }
},
},
'attio.lists': {
key: 'attio.lists',
contracts: [selectorContracts.attioListsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'attio.lists',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'attio.lists')
const data = await requestJson(selectorContracts.attioListsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.lists || []).map((list) => ({
id: list.id,
label: list.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'attio.lists')
const data = await requestJson(selectorContracts.attioListsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const list = (data.lists || []).find((l) => l.id === detailId) ?? null
if (!list) return null
return { id: list.id, label: list.name }
},
},
} satisfies Record<Extract<SelectorKey, 'attio.objects' | 'attio.lists'>, SelectorDefinition>
@@ -0,0 +1,111 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const bigquerySelectors = {
'bigquery.datasets': {
key: 'bigquery.datasets',
contracts: [selectorContracts.bigQueryDatasetsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'bigquery.datasets',
context.oauthCredential ?? 'none',
context.projectId ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.projectId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'bigquery.datasets')
if (!context.projectId) throw new Error('Missing project ID for bigquery.datasets selector')
const data = await requestJson(selectorContracts.bigQueryDatasetsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
return (data.datasets || []).map((ds) => ({
id: ds.datasetReference.datasetId,
label: ds.friendlyName || ds.datasetReference.datasetId,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId || !context.projectId) return null
const credentialId = ensureCredential(context, 'bigquery.datasets')
const data = await requestJson(selectorContracts.bigQueryDatasetsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
const ds =
(data.datasets || []).find((d) => d.datasetReference.datasetId === detailId) ?? null
if (!ds) return null
return {
id: ds.datasetReference.datasetId,
label: ds.friendlyName || ds.datasetReference.datasetId,
}
},
},
'bigquery.tables': {
key: 'bigquery.tables',
contracts: [selectorContracts.bigQueryTablesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'bigquery.tables',
context.oauthCredential ?? 'none',
context.projectId ?? 'none',
context.datasetId ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) =>
Boolean(context.oauthCredential && context.projectId && context.datasetId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'bigquery.tables')
if (!context.projectId) throw new Error('Missing project ID for bigquery.tables selector')
if (!context.datasetId) throw new Error('Missing dataset ID for bigquery.tables selector')
const data = await requestJson(selectorContracts.bigQueryTablesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
datasetId: context.datasetId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
return (data.tables || []).map((t) => ({
id: t.tableReference.tableId,
label: t.friendlyName || t.tableReference.tableId,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId || !context.projectId || !context.datasetId) return null
const credentialId = ensureCredential(context, 'bigquery.tables')
const data = await requestJson(selectorContracts.bigQueryTablesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
datasetId: context.datasetId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
const t = (data.tables || []).find((tbl) => tbl.tableReference.tableId === detailId) ?? null
if (!t) return null
return { id: t.tableReference.tableId, label: t.friendlyName || t.tableReference.tableId }
},
},
} satisfies Record<
Extract<SelectorKey, 'bigquery.datasets' | 'bigquery.tables'>,
SelectorDefinition
>
@@ -0,0 +1,76 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const calcomSelectors = {
'calcom.eventTypes': {
key: 'calcom.eventTypes',
contracts: [selectorContracts.calcomEventTypesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'calcom.eventTypes',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'calcom.eventTypes')
const data = await requestJson(selectorContracts.calcomEventTypesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.eventTypes || []).map((et) => ({
id: et.id,
label: et.title || et.slug,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'calcom.eventTypes')
const data = await requestJson(selectorContracts.calcomEventTypesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const et = (data.eventTypes || []).find((e) => e.id === detailId) ?? null
if (!et) return null
return { id: et.id, label: et.title || et.slug }
},
},
'calcom.schedules': {
key: 'calcom.schedules',
contracts: [selectorContracts.calcomSchedulesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'calcom.schedules',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'calcom.schedules')
const data = await requestJson(selectorContracts.calcomSchedulesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.schedules || []).map((s) => ({
id: s.id,
label: s.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'calcom.schedules')
const data = await requestJson(selectorContracts.calcomSchedulesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const s = (data.schedules || []).find((sc) => sc.id === detailId) ?? null
if (!s) return null
return { id: s.id, label: s.name }
},
},
} satisfies Record<
Extract<SelectorKey, 'calcom.eventTypes' | 'calcom.schedules'>,
SelectorDefinition
>
@@ -0,0 +1,94 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
function ensureAwsSelectorCredentials(context: SelectorQueryArgs['context'], key: SelectorKey) {
if (!context.awsAccessKeyId || !context.awsSecretAccessKey || !context.awsRegion) {
throw new Error(`Missing AWS credentials for selector ${key}`)
}
return {
accessKeyId: context.awsAccessKeyId,
secretAccessKey: context.awsSecretAccessKey,
region: context.awsRegion,
}
}
export const cloudwatchSelectors = {
'cloudwatch.logGroups': {
key: 'cloudwatch.logGroups',
contracts: [selectorContracts.cloudwatchLogGroupsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'cloudwatch.logGroups',
context.awsAccessKeyId ?? 'none',
context.awsRegion ?? 'none',
],
enabled: ({ context }) =>
Boolean(context.awsAccessKeyId && context.awsSecretAccessKey && context.awsRegion),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const awsCredentials = ensureAwsSelectorCredentials(context, 'cloudwatch.logGroups')
const data = await requestJson(selectorContracts.cloudwatchLogGroupsSelectorContract, {
body: {
...awsCredentials,
prefix: search,
},
signal,
})
return (data.output?.logGroups || []).map((lg) => ({
id: lg.logGroupName,
label: lg.logGroupName,
}))
},
fetchById: async ({ detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
return { id: detailId, label: detailId }
},
},
'cloudwatch.logStreams': {
key: 'cloudwatch.logStreams',
contracts: [selectorContracts.cloudwatchLogStreamsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'cloudwatch.logStreams',
context.awsAccessKeyId ?? 'none',
context.awsRegion ?? 'none',
context.logGroupName ?? 'none',
],
enabled: ({ context }) =>
Boolean(
context.awsAccessKeyId &&
context.awsSecretAccessKey &&
context.awsRegion &&
context.logGroupName
),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const awsCredentials = ensureAwsSelectorCredentials(context, 'cloudwatch.logStreams')
if (!context.logGroupName) {
throw new Error('Missing log group name for cloudwatch.logStreams selector')
}
const data = await requestJson(selectorContracts.cloudwatchLogStreamsSelectorContract, {
body: {
...awsCredentials,
logGroupName: context.logGroupName,
prefix: search,
},
signal,
})
return (data.output?.logStreams || []).map((ls) => ({
id: ls.logStreamName,
label: ls.logStreamName,
}))
},
fetchById: async ({ detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
return { id: detailId, label: detailId }
},
},
} satisfies Record<
Extract<SelectorKey, 'cloudwatch.logGroups' | 'cloudwatch.logStreams'>,
SelectorDefinition
>
@@ -0,0 +1,134 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { fetchOAuthToken } from '@/hooks/selectors/helpers'
import { ensureCredential, ensureDomain, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
function formatConfluenceSpaceLabel(space: { name: string; key: string; status?: string }): string {
const base = `${space.name} (${space.key})`
return space.status === 'archived' ? `${base} — archived` : base
}
function toSpaceOption(space: { name: string; key: string; status?: string }): {
id: string
label: string
} {
return { id: space.key, label: formatConfluenceSpaceLabel(space) }
}
export const confluenceSelectors = {
'confluence.spaces': {
key: 'confluence.spaces',
contracts: [selectorContracts.confluenceSpacesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'confluence.spaces',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
/**
* Drives pagination through {@link useSelectorOptions}, which drains every
* page via this callback. No `fetchList` — the paged path supersedes it.
*/
fetchPage: async ({ context, cursor, signal }) => {
const credentialId = ensureCredential(context, 'confluence.spaces')
const domain = ensureDomain(context, 'confluence.spaces')
const data = await requestJson(selectorContracts.confluenceSpacesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
domain,
cursor,
},
signal,
})
return {
items: (data.spaces || []).map(toSpaceOption),
nextCursor: data.nextCursor,
}
},
/**
* Resolves a single space label. Hits only the first page — the dropdown's
* `fetchPage` stream populates the options cache for spaces beyond page 1,
* and `useSelectorOptionMap` merges them in. Walking all pages here would
* double API load since the stream is already running in parallel.
*/
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'confluence.spaces')
const domain = ensureDomain(context, 'confluence.spaces')
const data = await requestJson(selectorContracts.confluenceSpacesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
domain,
},
signal,
})
const space = (data.spaces || []).find((s) => s.key === detailId) ?? null
if (!space) return null
return toSpaceOption(space)
},
},
'confluence.pages': {
key: 'confluence.pages',
contracts: [
selectorContracts.confluencePagesSelectorContract,
selectorContracts.confluencePageSelectorContract,
],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'confluence.pages',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'confluence.pages')
const domain = ensureDomain(context, 'confluence.pages')
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
if (!bundle) {
throw new Error('Missing Confluence access token')
}
const data = await requestJson(selectorContracts.confluencePagesSelectorContract, {
body: {
domain,
accessToken: bundle.accessToken,
cloudId: bundle.cloudId,
title: search,
},
signal,
})
return (data.files || []).map((file) => ({
id: file.id,
label: file.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'confluence.pages')
const domain = ensureDomain(context, 'confluence.pages')
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
if (!bundle) {
throw new Error('Missing Confluence access token')
}
const data = await requestJson(selectorContracts.confluencePageSelectorContract, {
body: {
domain,
accessToken: bundle.accessToken,
cloudId: bundle.cloudId,
pageId: detailId,
},
signal,
})
return { id: data.id, label: data.title }
},
},
} satisfies Record<
Extract<SelectorKey, 'confluence.spaces' | 'confluence.pages'>,
SelectorDefinition
>
@@ -0,0 +1,188 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const googleSelectors = {
'google.tasks.lists': {
key: 'google.tasks.lists',
contracts: [selectorContracts.googleTasksTaskListsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'google.tasks.lists',
context.oauthCredential ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'google.tasks.lists')
const data = await requestJson(selectorContracts.googleTasksTaskListsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
return (data.taskLists || []).map((tl) => ({ id: tl.id, label: tl.title }))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'google.tasks.lists')
const data = await requestJson(selectorContracts.googleTasksTaskListsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
const tl = (data.taskLists || []).find((t) => t.id === detailId) ?? null
if (!tl) return null
return { id: tl.id, label: tl.title }
},
},
'gmail.labels': {
key: 'gmail.labels',
contracts: [selectorContracts.gmailLabelsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'gmail.labels',
context.oauthCredential ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'gmail.labels')
const data = await requestJson(selectorContracts.gmailLabelsSelectorContract, {
query: {
credentialId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
return (data.labels || []).map((label) => ({
id: label.id,
label: label.name,
}))
},
},
'google.calendar': {
key: 'google.calendar',
contracts: [selectorContracts.googleCalendarSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'google.calendar',
context.oauthCredential ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'google.calendar')
const data = await requestJson(selectorContracts.googleCalendarSelectorContract, {
query: {
credentialId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
return (data.calendars || []).map((calendar) => ({
id: calendar.id,
label: calendar.summary,
}))
},
},
'google.drive': {
key: 'google.drive',
contracts: [
selectorContracts.googleDriveFilesSelectorContract,
selectorContracts.googleDriveFileSelectorContract,
],
staleTime: 15 * 1000,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'google.drive',
context.oauthCredential ?? 'none',
context.mimeType ?? 'any',
context.fileId ?? 'root',
search ?? '',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'google.drive')
const data = await requestJson(selectorContracts.googleDriveFilesSelectorContract, {
query: {
credentialId,
mimeType: context.mimeType,
parentId: context.fileId,
query: search,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
return (data.files || []).map((file) => ({
id: file.id,
label: file.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'google.drive')
const data = await requestJson(selectorContracts.googleDriveFileSelectorContract, {
query: {
credentialId,
fileId: detailId,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
const file = data.file
if (!file) return null
return { id: file.id, label: file.name }
},
},
'google.sheets': {
key: 'google.sheets',
contracts: [selectorContracts.googleSheetsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'google.sheets',
context.oauthCredential ?? 'none',
context.spreadsheetId ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'google.sheets')
if (!context.spreadsheetId) {
throw new Error('Missing spreadsheet ID for google.sheets selector')
}
const data = await requestJson(selectorContracts.googleSheetsSelectorContract, {
query: {
credentialId,
spreadsheetId: context.spreadsheetId,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
},
signal,
})
return (data.sheets || []).map((sheet) => ({
id: sheet.id,
label: sheet.name,
}))
},
},
} satisfies Record<
Extract<
SelectorKey,
'google.tasks.lists' | 'gmail.labels' | 'google.calendar' | 'google.drive' | 'google.sheets'
>,
SelectorDefinition
>
@@ -0,0 +1,134 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { fetchOAuthToken } from '@/hooks/selectors/helpers'
import { ensureCredential, ensureDomain, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const jiraSelectors = {
'jira.projects': {
key: 'jira.projects',
contracts: [
selectorContracts.jiraProjectsSelectorContract,
selectorContracts.jiraProjectSelectorContract,
],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'jira.projects',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jira.projects')
const domain = ensureDomain(context, 'jira.projects')
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
if (!bundle) {
throw new Error('Missing Jira access token')
}
const data = await requestJson(selectorContracts.jiraProjectsSelectorContract, {
query: {
domain,
accessToken: bundle.accessToken,
cloudId: bundle.cloudId,
query: search,
},
signal,
})
return (data.projects || []).map((project) => ({
id: project.id,
label: project.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'jira.projects')
const domain = ensureDomain(context, 'jira.projects')
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
if (!bundle) {
throw new Error('Missing Jira access token')
}
const data = await requestJson(selectorContracts.jiraProjectSelectorContract, {
body: {
domain,
accessToken: bundle.accessToken,
cloudId: bundle.cloudId,
projectId: detailId,
},
signal,
})
if (!data.project) return null
return {
id: data.project.id,
label: data.project.name,
}
},
},
'jira.issues': {
key: 'jira.issues',
contracts: [
selectorContracts.jiraIssuesSelectorContract,
selectorContracts.jiraIssueSelectorContract,
],
staleTime: 15 * 1000,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'jira.issues',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
context.projectId ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jira.issues')
const domain = ensureDomain(context, 'jira.issues')
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
if (!bundle) {
throw new Error('Missing Jira access token')
}
const data = await requestJson(selectorContracts.jiraIssuesSelectorContract, {
query: {
domain,
accessToken: bundle.accessToken,
cloudId: bundle.cloudId,
projectId: context.projectId,
query: search,
},
signal,
})
const issues =
data.sections?.flatMap((section) =>
(section.issues || []).map((issue) => ({
id: issue.id || issue.key || '',
name: issue.summary || issue.key || '',
}))
) || []
return issues
.filter((issue) => issue.id)
.map((issue) => ({ id: issue.id, label: issue.name || issue.id }))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'jira.issues')
const domain = ensureDomain(context, 'jira.issues')
const bundle = await fetchOAuthToken(credentialId, context.workflowId)
if (!bundle) {
throw new Error('Missing Jira access token')
}
const data = await requestJson(selectorContracts.jiraIssueSelectorContract, {
body: {
domain,
accessToken: bundle.accessToken,
cloudId: bundle.cloudId,
issueKeys: [detailId],
},
signal,
})
const issue = data.issues?.[0]
if (!issue) return null
return { id: issue.id, label: issue.name }
},
},
} satisfies Record<Extract<SelectorKey, 'jira.projects' | 'jira.issues'>, SelectorDefinition>
@@ -0,0 +1,104 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, ensureDomain, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const jsmSelectors = {
'jsm.serviceDesks': {
key: 'jsm.serviceDesks',
contracts: [selectorContracts.jsmServiceDesksSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'jsm.serviceDesks',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jsm.serviceDesks')
const domain = ensureDomain(context, 'jsm.serviceDesks')
const data = await requestJson(selectorContracts.jsmServiceDesksSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
domain,
},
signal,
})
return (data.serviceDesks || []).map((sd) => ({
id: sd.id,
label: sd.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'jsm.serviceDesks')
const domain = ensureDomain(context, 'jsm.serviceDesks')
const data = await requestJson(selectorContracts.jsmServiceDesksSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
domain,
},
signal,
})
const sd = (data.serviceDesks || []).find((s) => s.id === detailId) ?? null
if (!sd) return null
return { id: sd.id, label: sd.name }
},
},
'jsm.requestTypes': {
key: 'jsm.requestTypes',
contracts: [selectorContracts.jsmRequestTypesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'jsm.requestTypes',
context.oauthCredential ?? 'none',
context.domain ?? 'none',
context.serviceDeskId ?? 'none',
],
enabled: ({ context }) =>
Boolean(context.oauthCredential && context.domain && context.serviceDeskId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'jsm.requestTypes')
const domain = ensureDomain(context, 'jsm.requestTypes')
if (!context.serviceDeskId) throw new Error('Missing serviceDeskId for jsm.requestTypes')
const data = await requestJson(selectorContracts.jsmRequestTypesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
domain,
serviceDeskId: context.serviceDeskId,
},
signal,
})
return (data.requestTypes || []).map((rt) => ({
id: rt.id,
label: rt.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'jsm.requestTypes')
const domain = ensureDomain(context, 'jsm.requestTypes')
if (!context.serviceDeskId) return null
const data = await requestJson(selectorContracts.jsmRequestTypesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
domain,
serviceDeskId: context.serviceDeskId,
},
signal,
})
const rt = (data.requestTypes || []).find((r) => r.id === detailId) ?? null
if (!rt) return null
return { id: rt.id, label: rt.name }
},
},
} satisfies Record<
Extract<SelectorKey, 'jsm.serviceDesks' | 'jsm.requestTypes'>,
SelectorDefinition
>
@@ -0,0 +1,61 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureKnowledgeBase, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
const KNOWLEDGE_DOCUMENTS_PAGE_LIMIT = 100
export const knowledgeSelectors = {
'knowledge.documents': {
key: 'knowledge.documents',
contracts: [
selectorContracts.listKnowledgeSelectorDocumentsContract,
selectorContracts.getKnowledgeSelectorDocumentContract,
],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'knowledge.documents',
context.knowledgeBaseId ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.knowledgeBaseId),
/**
* Drives pagination through {@link useSelectorOptions}, which drains every
* page via this callback. The `pagination.hasMore` flag from the route
* decides when to stop; `nextCursor` encodes the next `offset`.
*/
fetchPage: async ({ context, search, cursor, signal }) => {
const knowledgeBaseId = ensureKnowledgeBase(context)
const offset = cursor ? Number(cursor) : 0
const result = await requestJson(selectorContracts.listKnowledgeSelectorDocumentsContract, {
params: { id: knowledgeBaseId },
query: {
limit: KNOWLEDGE_DOCUMENTS_PAGE_LIMIT,
offset,
search,
},
signal,
})
const { pagination } = result.data
const nextOffset = pagination.offset + pagination.limit
return {
items: result.data.documents.map((doc) => ({
id: doc.id,
label: doc.filename,
})),
nextCursor: pagination.hasMore ? String(nextOffset) : undefined,
}
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const knowledgeBaseId = ensureKnowledgeBase(context)
const result = await requestJson(selectorContracts.getKnowledgeSelectorDocumentContract, {
params: { id: knowledgeBaseId, documentId: detailId },
query: { includeDisabled: 'true' },
signal,
})
return { id: result.data.id, label: result.data.filename }
},
},
} satisfies Record<Extract<SelectorKey, 'knowledge.documents'>, SelectorDefinition>
@@ -0,0 +1,59 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const linearSelectors = {
'linear.teams': {
key: 'linear.teams',
contracts: [selectorContracts.linearTeamsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'linear.teams',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'linear.teams')
const data = await requestJson(selectorContracts.linearTeamsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.teams || []).map((team) => ({
id: team.id,
label: team.name,
}))
},
},
'linear.projects': {
key: 'linear.projects',
contracts: [selectorContracts.linearProjectsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'linear.projects',
context.oauthCredential ?? 'none',
context.teamId ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'linear.projects')
if (!context.teamId) {
throw new Error('Missing team ID for linear.projects selector')
}
const data = await requestJson(selectorContracts.linearProjectsSelectorContract, {
body: {
credential: credentialId,
teamId: context.teamId,
workflowId: context.workflowId,
},
signal,
})
return (data.projects || []).map((project) => ({
id: project.id,
label: project.name,
}))
},
},
} satisfies Record<Extract<SelectorKey, 'linear.teams' | 'linear.projects'>, SelectorDefinition>
@@ -0,0 +1,378 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const microsoftSelectors = {
'microsoft.planner.plans': {
key: 'microsoft.planner.plans',
contracts: [selectorContracts.microsoftPlannerPlansSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.planner.plans',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.planner.plans')
const data = await requestJson(selectorContracts.microsoftPlannerPlansSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.plans || []).map((plan) => ({ id: plan.id, label: plan.title }))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'microsoft.planner.plans')
const data = await requestJson(selectorContracts.microsoftPlannerPlansSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const plan = (data.plans || []).find((p) => p.id === detailId) ?? null
if (!plan) return null
return { id: plan.id, label: plan.title }
},
},
'outlook.folders': {
key: 'outlook.folders',
contracts: [selectorContracts.outlookFoldersSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'outlook.folders',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'outlook.folders')
const data = await requestJson(selectorContracts.outlookFoldersSelectorContract, {
query: { credentialId },
signal,
})
return (data.folders || []).map((folder) => ({
id: folder.id,
label: folder.name,
}))
},
},
'microsoft.teams': {
key: 'microsoft.teams',
contracts: [selectorContracts.microsoftTeamsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.teams',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.teams')
const data = await requestJson(selectorContracts.microsoftTeamsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.teams || []).map((team) => ({
id: team.id,
label: team.displayName,
}))
},
},
'microsoft.chats': {
key: 'microsoft.chats',
contracts: [selectorContracts.microsoftChatsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.chats',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.chats')
const data = await requestJson(selectorContracts.microsoftChatsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.chats || []).map((chat) => ({
id: chat.id,
label: chat.displayName,
}))
},
},
'microsoft.channels': {
key: 'microsoft.channels',
contracts: [selectorContracts.microsoftChannelsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.channels',
context.oauthCredential ?? 'none',
context.teamId ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.channels')
if (!context.teamId) {
throw new Error('Missing team ID for microsoft.channels selector')
}
const data = await requestJson(selectorContracts.microsoftChannelsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
teamId: context.teamId,
},
signal,
})
return (data.channels || []).map((channel) => ({
id: channel.id,
label: channel.displayName,
}))
},
},
'microsoft.planner': {
key: 'microsoft.planner',
contracts: [selectorContracts.microsoftPlannerTasksSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.planner',
context.oauthCredential ?? 'none',
context.planId ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.planId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.planner')
if (!context.planId) {
throw new Error('Missing plan ID for microsoft.planner selector')
}
const data = await requestJson(selectorContracts.microsoftPlannerTasksSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
planId: context.planId,
},
signal,
})
return (data.tasks || []).map((task) => ({
id: task.id,
label: task.title,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId || !context.planId) return null
const credentialId = ensureCredential(context, 'microsoft.planner')
const data = await requestJson(selectorContracts.microsoftPlannerTasksSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
planId: context.planId,
},
signal,
})
const task = (data.tasks || []).find((t) => t.id === detailId) ?? null
if (!task) return null
return { id: task.id, label: task.title }
},
},
'onedrive.files': {
key: 'onedrive.files',
contracts: [selectorContracts.onedriveFilesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'onedrive.files',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'onedrive.files')
const data = await requestJson(selectorContracts.onedriveFilesSelectorContract, {
query: { credentialId },
signal,
})
return (data.files || []).map((file) => ({
id: file.id,
label: file.name,
}))
},
},
'onedrive.folders': {
key: 'onedrive.folders',
contracts: [selectorContracts.onedriveFoldersSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'onedrive.folders',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'onedrive.folders')
const data = await requestJson(selectorContracts.onedriveFoldersSelectorContract, {
query: { credentialId },
signal,
})
return (data.files || []).map((file) => ({
id: file.id,
label: file.name,
}))
},
},
'microsoft.excel.sheets': {
key: 'microsoft.excel.sheets',
contracts: [selectorContracts.microsoftExcelSheetsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.excel.sheets',
context.oauthCredential ?? 'none',
context.spreadsheetId ?? 'none',
context.driveId ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.excel.sheets')
if (!context.spreadsheetId) {
throw new Error('Missing spreadsheet ID for microsoft.excel.sheets selector')
}
const data = await requestJson(selectorContracts.microsoftExcelSheetsSelectorContract, {
query: {
credentialId,
spreadsheetId: context.spreadsheetId,
driveId: context.driveId,
workflowId: context.workflowId,
},
signal,
})
return (data.sheets || []).map((sheet) => ({
id: sheet.id,
label: sheet.name,
}))
},
},
'microsoft.excel.drives': {
key: 'microsoft.excel.drives',
contracts: [
selectorContracts.microsoftExcelDrivesSelectorContract,
selectorContracts.microsoftExcelDriveSelectorContract,
],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'microsoft.excel.drives',
context.oauthCredential ?? 'none',
context.siteId ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.excel.drives')
if (!context.siteId) {
throw new Error('Missing site ID for microsoft.excel.drives selector')
}
const data = await requestJson(selectorContracts.microsoftExcelDrivesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
siteId: context.siteId,
},
signal,
})
return data.drives.map((drive) => ({
id: drive.id,
label: drive.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId || !context.siteId) return null
const credentialId = ensureCredential(context, 'microsoft.excel.drives')
const data = await requestJson(selectorContracts.microsoftExcelDriveSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
siteId: context.siteId,
driveId: detailId,
},
signal,
})
const { drive } = data
if (!drive) return null
return { id: drive.id, label: drive.name }
},
},
'microsoft.excel': {
key: 'microsoft.excel',
contracts: [selectorContracts.microsoftFilesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'microsoft.excel',
context.oauthCredential ?? 'none',
context.driveId ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.excel')
const data = await requestJson(selectorContracts.microsoftFilesSelectorContract, {
query: {
credentialId,
query: search,
driveId: context.driveId,
workflowId: context.workflowId,
fileType: 'excel',
},
signal,
})
return (data.files || []).map((file) => ({
id: file.id,
label: file.name,
}))
},
},
'microsoft.word': {
key: 'microsoft.word',
contracts: [selectorContracts.microsoftFilesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'microsoft.word',
context.oauthCredential ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'microsoft.word')
const data = await requestJson(selectorContracts.microsoftFilesSelectorContract, {
query: {
credentialId,
query: search,
workflowId: context.workflowId,
fileType: 'word',
},
signal,
})
return (data.files || []).map((file) => ({
id: file.id,
label: file.name,
}))
},
},
} satisfies Record<
Extract<
SelectorKey,
| 'microsoft.planner.plans'
| 'outlook.folders'
| 'microsoft.teams'
| 'microsoft.chats'
| 'microsoft.channels'
| 'microsoft.planner'
| 'onedrive.files'
| 'onedrive.folders'
| 'microsoft.excel.sheets'
| 'microsoft.excel.drives'
| 'microsoft.excel'
| 'microsoft.word'
>,
SelectorDefinition
>
@@ -0,0 +1,86 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const mondaySelectors = {
'monday.boards': {
key: 'monday.boards',
contracts: [selectorContracts.mondayBoardsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'monday.boards',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'monday.boards')
const data = await requestJson(selectorContracts.mondayBoardsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.boards || []).map((board) => ({
id: board.id,
label: board.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'monday.boards')
const data = await requestJson(selectorContracts.mondayBoardsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const board = (data.boards || []).find((b) => b.id === detailId) ?? null
if (!board) return null
return { id: board.id, label: board.name }
},
},
'monday.groups': {
key: 'monday.groups',
contracts: [selectorContracts.mondayGroupsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'monday.groups',
context.oauthCredential ?? 'none',
context.boardId ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.boardId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'monday.groups')
if (!context.boardId) {
throw new Error('Missing board ID for monday.groups selector')
}
const data = await requestJson(selectorContracts.mondayGroupsSelectorContract, {
body: {
credential: credentialId,
boardId: context.boardId,
workflowId: context.workflowId,
},
signal,
})
return (data.groups || []).map((group) => ({
id: group.id,
label: group.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'monday.groups')
if (!context.boardId) return null
const data = await requestJson(selectorContracts.mondayGroupsSelectorContract, {
body: {
credential: credentialId,
boardId: context.boardId,
workflowId: context.workflowId,
},
signal,
})
const group = (data.groups || []).find((g) => g.id === detailId) ?? null
if (!group) return null
return { id: group.id, label: group.name }
},
},
} satisfies Record<Extract<SelectorKey, 'monday.boards' | 'monday.groups'>, SelectorDefinition>
@@ -0,0 +1,73 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const notionSelectors = {
'notion.databases': {
key: 'notion.databases',
contracts: [selectorContracts.notionDatabasesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'notion.databases',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'notion.databases')
const data = await requestJson(selectorContracts.notionDatabasesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.databases || []).map((db) => ({
id: db.id,
label: db.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'notion.databases')
const data = await requestJson(selectorContracts.notionDatabasesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const db = (data.databases || []).find((d) => d.id === detailId) ?? null
if (!db) return null
return { id: db.id, label: db.name }
},
},
'notion.pages': {
key: 'notion.pages',
contracts: [selectorContracts.notionPagesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'notion.pages',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'notion.pages')
const data = await requestJson(selectorContracts.notionPagesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.pages || []).map((page) => ({
id: page.id,
label: page.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'notion.pages')
const data = await requestJson(selectorContracts.notionPagesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const page = (data.pages || []).find((p) => p.id === detailId) ?? null
if (!page) return null
return { id: page.id, label: page.name }
},
},
} satisfies Record<Extract<SelectorKey, 'notion.databases' | 'notion.pages'>, SelectorDefinition>
@@ -0,0 +1,40 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const pipedriveSelectors = {
'pipedrive.pipelines': {
key: 'pipedrive.pipelines',
contracts: [selectorContracts.pipedrivePipelinesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'pipedrive.pipelines',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'pipedrive.pipelines')
const data = await requestJson(selectorContracts.pipedrivePipelinesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.pipelines || []).map((p) => ({
id: p.id,
label: p.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'pipedrive.pipelines')
const data = await requestJson(selectorContracts.pipedrivePipelinesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const p = (data.pipelines || []).find((pl) => pl.id === detailId) ?? null
if (!p) return null
return { id: p.id, label: p.name }
},
},
} satisfies Record<Extract<SelectorKey, 'pipedrive.pipelines'>, SelectorDefinition>
@@ -0,0 +1,24 @@
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
export const SELECTOR_STALE = 60 * 1000
export const ensureCredential = (context: SelectorContext, key: SelectorKey): string => {
if (!context.oauthCredential) {
throw new Error(`Missing credential for selector ${key}`)
}
return context.oauthCredential
}
export const ensureDomain = (context: SelectorContext, key: SelectorKey): string => {
if (!context.domain) {
throw new Error(`Missing domain for selector ${key}`)
}
return context.domain
}
export const ensureKnowledgeBase = (context: SelectorContext): string => {
if (!context.knowledgeBaseId) {
throw new Error('Missing knowledge base id')
}
return context.knowledgeBaseId
}
@@ -0,0 +1,89 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const sharepointSelectors = {
'sharepoint.lists': {
key: 'sharepoint.lists',
contracts: [selectorContracts.sharepointListsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'sharepoint.lists',
context.oauthCredential ?? 'none',
context.siteId ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'sharepoint.lists')
if (!context.siteId) throw new Error('Missing site ID for sharepoint.lists selector')
const data = await requestJson(selectorContracts.sharepointListsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
siteId: context.siteId,
},
signal,
})
return (data.lists || []).map((list) => ({ id: list.id, label: list.displayName }))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId || !context.siteId) return null
const credentialId = ensureCredential(context, 'sharepoint.lists')
const data = await requestJson(selectorContracts.sharepointListsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
siteId: context.siteId,
},
signal,
})
const list = (data.lists || []).find((l) => l.id === detailId) ?? null
if (!list) return null
return { id: list.id, label: list.displayName }
},
},
'sharepoint.sites': {
key: 'sharepoint.sites',
contracts: [selectorContracts.sharepointSitesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'sharepoint.sites',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'sharepoint.sites')
const data = await requestJson(selectorContracts.sharepointSitesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
},
signal,
})
return (data.files || []).map((file) => ({
id: file.id,
label: file.name,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'sharepoint.sites')
const data = await requestJson(selectorContracts.sharepointSitesSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
},
signal,
})
const site = (data.files || []).find((f) => f.id === detailId) ?? null
if (!site) return null
return { id: site.id, label: site.name }
},
},
} satisfies Record<
Extract<SelectorKey, 'sharepoint.lists' | 'sharepoint.sites'>,
SelectorDefinition
>
@@ -0,0 +1,119 @@
import { getColumnId } from '@/lib/table/column-keys'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { getTableDetailQueryOptions } from '@/hooks/queries/tables'
import { getFolderMap } from '@/hooks/queries/utils/folder-cache'
import { getFolderPath } from '@/hooks/queries/utils/folder-tree'
import { getWorkflowById, getWorkflows } from '@/hooks/queries/utils/workflow-cache'
import { getWorkflowListQueryOptions } from '@/hooks/queries/utils/workflow-list-query'
import { SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import { selectorKeys } from '@/hooks/selectors/query-keys'
import type {
SelectorDefinition,
SelectorKey,
SelectorOption,
SelectorQueryArgs,
} from '@/hooks/selectors/types'
import type { WorkflowFolder } from '@/stores/folders/types'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
/**
* Builds a label for a workflow option, appending the folder path when another
* workflow in the workspace shares the same display name. Avoids suffixing
* every workflow so the dropdown stays readable for the common case.
*/
function buildDisambiguatedLabel(
workflow: WorkflowMetadata,
duplicateNames: Set<string>,
folders: Record<string, WorkflowFolder>
): string {
const baseLabel = workflow.name || `Workflow ${workflow.id.slice(0, 8)}`
if (!duplicateNames.has(baseLabel)) return baseLabel
const folderPath = getFolderPath(workflow.folderId, folders)
return folderPath ? `${baseLabel} (${folderPath})` : `${baseLabel} (Root)`
}
function collectDuplicateNames(workflows: WorkflowMetadata[]): Set<string> {
const counts = new Map<string, number>()
for (const workflow of workflows) {
const label = workflow.name || `Workflow ${workflow.id.slice(0, 8)}`
counts.set(label, (counts.get(label) ?? 0) + 1)
}
const duplicates = new Set<string>()
for (const [label, count] of counts) {
if (count > 1) duplicates.add(label)
}
return duplicates
}
export const simSelectors = {
'sim.workflows': {
key: 'sim.workflows',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) =>
context.workspaceId
? selectorKeys.simWorkflows(context.workspaceId, context.excludeWorkflowId)
: [...selectorKeys.all, 'sim.workflows', 'none', context.excludeWorkflowId ?? 'none'],
enabled: ({ context }) => Boolean(context.workspaceId),
fetchList: async ({ context, signal }: SelectorQueryArgs): Promise<SelectorOption[]> => {
if (!context.workspaceId) return []
await getQueryClient().ensureQueryData(getWorkflowListQueryOptions(context.workspaceId))
const workflows = getWorkflows(context.workspaceId)
const folders = getFolderMap(context.workspaceId)
const duplicateNames = collectDuplicateNames(workflows)
return workflows
.filter((w) => w.id !== context.excludeWorkflowId)
.map((w) => ({
id: w.id,
label: buildDisambiguatedLabel(w, duplicateNames, folders),
}))
.sort((a, b) => a.label.localeCompare(b.label))
},
fetchById: async ({
context,
detailId,
signal,
}: SelectorQueryArgs): Promise<SelectorOption | null> => {
if (!detailId || !context.workspaceId) return null
await getQueryClient().ensureQueryData(getWorkflowListQueryOptions(context.workspaceId))
const workflow = getWorkflowById(context.workspaceId, detailId)
if (!workflow) return null
const workflows = getWorkflows(context.workspaceId)
const folders = getFolderMap(context.workspaceId)
const duplicateNames = collectDuplicateNames(workflows)
return {
id: detailId,
label: buildDisambiguatedLabel(workflow, duplicateNames, folders),
}
},
},
'table.columns': {
key: 'table.columns',
staleTime: SELECTOR_STALE,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
...selectorKeys.all,
'table.columns',
context.workspaceId ?? 'none',
context.tableId ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.workspaceId && context.tableId),
fetchList: async ({ context }: SelectorQueryArgs): Promise<SelectorOption[]> => {
if (!context.workspaceId || !context.tableId) return []
const table = await getQueryClient().ensureQueryData(
getTableDetailQueryOptions(context.workspaceId, context.tableId)
)
return (table.schema?.columns ?? [])
.filter((col) => col.unique)
.map((col) => ({ id: getColumnId(col), label: col.name }))
},
fetchById: async ({ context, detailId }: SelectorQueryArgs): Promise<SelectorOption | null> => {
if (!detailId || !context.workspaceId || !context.tableId) return null
const table = await getQueryClient().ensureQueryData(
getTableDetailQueryOptions(context.workspaceId, context.tableId)
)
const col = (table.schema?.columns ?? []).find((c) => getColumnId(c) === detailId)
return col ? { id: getColumnId(col), label: col.name } : null
},
},
} satisfies Record<Extract<SelectorKey, 'sim.workflows' | 'table.columns'>, SelectorDefinition>
@@ -0,0 +1,57 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const slackSelectors = {
'slack.channels': {
key: 'slack.channels',
contracts: [selectorContracts.slackChannelsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'slack.channels',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'slack.channels')
const data = await requestJson(selectorContracts.slackChannelsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
},
signal,
})
return (data.channels || []).map((channel) => ({
id: channel.id,
label: `#${channel.name}`,
}))
},
},
'slack.users': {
key: 'slack.users',
contracts: [selectorContracts.slackUsersSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'slack.users',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'slack.users')
const data = await requestJson(selectorContracts.slackUsersSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
},
signal,
})
return data.users.map((user) => ({
id: user.id,
label: user.real_name || user.name,
}))
},
},
} satisfies Record<Extract<SelectorKey, 'slack.channels' | 'slack.users'>, SelectorDefinition>
@@ -0,0 +1,39 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const trelloSelectors = {
'trello.boards': {
key: 'trello.boards',
contracts: [selectorContracts.trelloBoardsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'trello.boards',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'trello.boards')
const data = await requestJson(selectorContracts.trelloBoardsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.boards || [])
.filter((board) => !board.closed)
.map((board) => ({ id: board.id, label: board.name }))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'trello.boards')
const data = await requestJson(selectorContracts.trelloBoardsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const board = (data.boards || []).find((b) => b.id === detailId) ?? null
if (!board) return null
return { id: board.id, label: board.name }
},
},
} satisfies Record<Extract<SelectorKey, 'trello.boards'>, SelectorDefinition>
@@ -0,0 +1,30 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const wealthboxSelectors = {
'wealthbox.contacts': {
key: 'wealthbox.contacts',
contracts: [selectorContracts.wealthboxItemsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'wealthbox.contacts',
context.oauthCredential ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'wealthbox.contacts')
const data = await requestJson(selectorContracts.wealthboxItemsSelectorContract, {
query: { credentialId, type: 'contact', query: search ?? '' },
signal,
})
return (data.items || []).map((item) => ({
id: item.id,
label: item.name,
}))
},
},
} satisfies Record<Extract<SelectorKey, 'wealthbox.contacts'>, SelectorDefinition>
@@ -0,0 +1,94 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const webflowSelectors = {
'webflow.sites': {
key: 'webflow.sites',
contracts: [selectorContracts.webflowSitesSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'webflow.sites',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'webflow.sites')
const data = await requestJson(selectorContracts.webflowSitesSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.sites || []).map((site) => ({
id: site.id,
label: site.name,
}))
},
},
'webflow.collections': {
key: 'webflow.collections',
contracts: [selectorContracts.webflowCollectionsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'webflow.collections',
context.oauthCredential ?? 'none',
context.siteId ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'webflow.collections')
if (!context.siteId) {
throw new Error('Missing site ID for webflow.collections selector')
}
const data = await requestJson(selectorContracts.webflowCollectionsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
siteId: context.siteId,
},
signal,
})
return (data.collections || []).map((collection) => ({
id: collection.id,
label: collection.name,
}))
},
},
'webflow.items': {
key: 'webflow.items',
contracts: [selectorContracts.webflowItemsSelectorContract],
staleTime: 15 * 1000,
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
'selectors',
'webflow.items',
context.oauthCredential ?? 'none',
context.collectionId ?? 'none',
search ?? '',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.collectionId),
fetchList: async ({ context, search, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'webflow.items')
if (!context.collectionId) {
throw new Error('Missing collection ID for webflow.items selector')
}
const data = await requestJson(selectorContracts.webflowItemsSelectorContract, {
body: {
credential: credentialId,
workflowId: context.workflowId,
collectionId: context.collectionId,
search,
},
signal,
})
return (data.items || []).map((item) => ({
id: item.id,
label: item.name,
}))
},
},
} satisfies Record<
Extract<SelectorKey, 'webflow.sites' | 'webflow.collections' | 'webflow.items'>,
SelectorDefinition
>
@@ -0,0 +1,40 @@
import { requestJson } from '@/lib/api/client/request'
import * as selectorContracts from '@/lib/api/contracts/selectors'
import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared'
import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types'
export const zoomSelectors = {
'zoom.meetings': {
key: 'zoom.meetings',
contracts: [selectorContracts.zoomMeetingsSelectorContract],
staleTime: SELECTOR_STALE,
getQueryKey: ({ context }: SelectorQueryArgs) => [
'selectors',
'zoom.meetings',
context.oauthCredential ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, signal }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'zoom.meetings')
const data = await requestJson(selectorContracts.zoomMeetingsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
return (data.meetings || []).map((m) => ({
id: m.id,
label: m.name || `Meeting ${m.id}`,
}))
},
fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'zoom.meetings')
const data = await requestJson(selectorContracts.zoomMeetingsSelectorContract, {
body: { credential: credentialId, workflowId: context.workflowId },
signal,
})
const meeting = (data.meetings || []).find((m) => m.id === detailId) ?? null
if (!meeting) return null
return { id: meeting.id, label: meeting.name || `Meeting ${meeting.id}` }
},
},
} satisfies Record<Extract<SelectorKey, 'zoom.meetings'>, SelectorDefinition>