@yolk-sdk/connectors 0.0.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/action.d.mts +37 -0
- package/dist/action.d.mts.map +1 -0
- package/dist/action.mjs +24 -0
- package/dist/action.mjs.map +1 -0
- package/dist/agent.d.mts +21 -0
- package/dist/agent.d.mts.map +1 -0
- package/dist/agent.mjs +66 -0
- package/dist/agent.mjs.map +1 -0
- package/dist/config.d.mts +10 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +21 -0
- package/dist/config.mjs.map +1 -0
- package/dist/connector.d.mts +27 -0
- package/dist/connector.d.mts.map +1 -0
- package/dist/connector.mjs +32 -0
- package/dist/connector.mjs.map +1 -0
- package/dist/credential.d.mts +62 -0
- package/dist/credential.d.mts.map +1 -0
- package/dist/credential.mjs +62 -0
- package/dist/credential.mjs.map +1 -0
- package/dist/error.d.mts +17 -0
- package/dist/error.d.mts.map +1 -0
- package/dist/error.mjs +22 -0
- package/dist/error.mjs.map +1 -0
- package/dist/figma/index.d.mts +48 -0
- package/dist/figma/index.d.mts.map +1 -0
- package/dist/figma/index.mjs +97 -0
- package/dist/figma/index.mjs.map +1 -0
- package/dist/google/calendar.d.mts +53 -0
- package/dist/google/calendar.d.mts.map +1 -0
- package/dist/google/calendar.mjs +111 -0
- package/dist/google/calendar.mjs.map +1 -0
- package/dist/google/gmail.d.mts +53 -0
- package/dist/google/gmail.d.mts.map +1 -0
- package/dist/google/gmail.mjs +103 -0
- package/dist/google/gmail.mjs.map +1 -0
- package/dist/google/index.d.mts +14 -0
- package/dist/google/index.d.mts.map +1 -0
- package/dist/google/index.mjs +15 -0
- package/dist/google/index.mjs.map +1 -0
- package/dist/google/oauth.d.mts +18 -0
- package/dist/google/oauth.d.mts.map +1 -0
- package/dist/google/oauth.mjs +25 -0
- package/dist/google/oauth.mjs.map +1 -0
- package/dist/google/shared.d.mts +20 -0
- package/dist/google/shared.d.mts.map +1 -0
- package/dist/google/shared.mjs +36 -0
- package/dist/google/shared.mjs.map +1 -0
- package/dist/http.d.mts +32 -0
- package/dist/http.d.mts.map +1 -0
- package/dist/http.mjs +36 -0
- package/dist/http.mjs.map +1 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.mjs +9 -0
- package/dist/integration.d.mts +24 -0
- package/dist/integration.d.mts.map +1 -0
- package/dist/integration.mjs +22 -0
- package/dist/integration.mjs.map +1 -0
- package/dist/linkedin-search/index.d.mts +60 -0
- package/dist/linkedin-search/index.d.mts.map +1 -0
- package/dist/linkedin-search/index.mjs +162 -0
- package/dist/linkedin-search/index.mjs.map +1 -0
- package/dist/notion/index.d.mts +69 -0
- package/dist/notion/index.d.mts.map +1 -0
- package/dist/notion/index.mjs +169 -0
- package/dist/notion/index.mjs.map +1 -0
- package/dist/r2-storage/index.d.mts +48 -0
- package/dist/r2-storage/index.d.mts.map +1 -0
- package/dist/r2-storage/index.mjs +91 -0
- package/dist/r2-storage/index.mjs.map +1 -0
- package/dist/result.d.mts +32 -0
- package/dist/result.d.mts.map +1 -0
- package/dist/result.mjs +23 -0
- package/dist/result.mjs.map +1 -0
- package/dist/telegram/index.d.mts +34 -0
- package/dist/telegram/index.d.mts.map +1 -0
- package/dist/telegram/index.mjs +109 -0
- package/dist/telegram/index.mjs.map +1 -0
- package/dist/todoist/index.d.mts +70 -0
- package/dist/todoist/index.d.mts.map +1 -0
- package/dist/todoist/index.mjs +176 -0
- package/dist/todoist/index.mjs.map +1 -0
- package/package.json +96 -0
- package/src/action.ts +75 -0
- package/src/agent.ts +120 -0
- package/src/config.ts +28 -0
- package/src/connector.ts +62 -0
- package/src/credential.ts +86 -0
- package/src/error.ts +20 -0
- package/src/figma/index.ts +121 -0
- package/src/google/calendar.ts +145 -0
- package/src/google/gmail.ts +127 -0
- package/src/google/index.ts +46 -0
- package/src/google/oauth.ts +26 -0
- package/src/google/shared.ts +56 -0
- package/src/http.ts +51 -0
- package/src/index.ts +36 -0
- package/src/integration.ts +28 -0
- package/src/linkedin-search/index.ts +217 -0
- package/src/notion/index.ts +234 -0
- package/src/r2-storage/index.ts +118 -0
- package/src/result.ts +35 -0
- package/src/telegram/index.ts +144 -0
- package/src/todoist/index.ts +227 -0
package/src/http.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Context, Effect } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import { ConnectorError } from './error.ts'
|
|
4
|
+
|
|
5
|
+
export const HttpMethod = Schema.Literals(['GET', 'POST', 'PATCH', 'PUT', 'DELETE'])
|
|
6
|
+
export type HttpMethod = typeof HttpMethod.Type
|
|
7
|
+
|
|
8
|
+
export class ConnectorHttpRequest extends Schema.Class<ConnectorHttpRequest>('ConnectorHttpRequest')({
|
|
9
|
+
method: HttpMethod,
|
|
10
|
+
url: Schema.String,
|
|
11
|
+
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
12
|
+
body: Schema.optional(Schema.String)
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
export class ConnectorHttpResponse extends Schema.Class<ConnectorHttpResponse>('ConnectorHttpResponse')({
|
|
16
|
+
status: Schema.Number,
|
|
17
|
+
headers: Schema.Record(Schema.String, Schema.String),
|
|
18
|
+
body: Schema.String
|
|
19
|
+
}) {}
|
|
20
|
+
|
|
21
|
+
export type ConnectorHttpClientApi = {
|
|
22
|
+
readonly request: (request: ConnectorHttpRequest) => Effect.Effect<ConnectorHttpResponse, ConnectorError>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ConnectorHttpClient extends Context.Service<ConnectorHttpClient, ConnectorHttpClientApi>()(
|
|
26
|
+
'@yolk-sdk/connectors/ConnectorHttpClient'
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
type JsonResponseSchema<A> = Schema.Schema<A> & { readonly DecodingServices: never }
|
|
30
|
+
|
|
31
|
+
export const decodeJsonResponse = <A>(schema: JsonResponseSchema<A>, response: ConnectorHttpResponse) =>
|
|
32
|
+
Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)(response.body).pipe(
|
|
33
|
+
Effect.mapError(error =>
|
|
34
|
+
new ConnectorError({
|
|
35
|
+
cause: 'validation_failed',
|
|
36
|
+
message: 'Invalid JSON response',
|
|
37
|
+
underlying: error
|
|
38
|
+
})
|
|
39
|
+
),
|
|
40
|
+
Effect.flatMap(value =>
|
|
41
|
+
Schema.decodeUnknownEffect(schema)(value).pipe(
|
|
42
|
+
Effect.mapError(error =>
|
|
43
|
+
new ConnectorError({
|
|
44
|
+
cause: 'validation_failed',
|
|
45
|
+
message: 'Invalid response shape',
|
|
46
|
+
underlying: error
|
|
47
|
+
})
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
)
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export { defineAction } from './action.ts'
|
|
2
|
+
export type {
|
|
3
|
+
ActionExecutionInput,
|
|
4
|
+
ConnectorAction,
|
|
5
|
+
DefineActionOptions,
|
|
6
|
+
UnknownActionExecutionInput
|
|
7
|
+
} from './action.ts'
|
|
8
|
+
export { defineConnector } from './connector.ts'
|
|
9
|
+
export type { Connector, ConnectorInvokeInput, DefineConnectorOptions } from './connector.ts'
|
|
10
|
+
export {
|
|
11
|
+
ApiKeyCredential,
|
|
12
|
+
BearerTokenCredential,
|
|
13
|
+
CredentialBinding,
|
|
14
|
+
CredentialKind,
|
|
15
|
+
CredentialResolver,
|
|
16
|
+
CredentialSlot,
|
|
17
|
+
OAuthCredential,
|
|
18
|
+
RuntimeCredential,
|
|
19
|
+
findCredentialBinding,
|
|
20
|
+
makeCredentialBinding,
|
|
21
|
+
resolveCredential
|
|
22
|
+
} from './credential.ts'
|
|
23
|
+
export type { CredentialResolveRequest, CredentialResolverApi } from './credential.ts'
|
|
24
|
+
export { ConnectorError, ConnectorErrorCause } from './error.ts'
|
|
25
|
+
export { optionalStringConfig, requiredStringConfig } from './config.ts'
|
|
26
|
+
export {
|
|
27
|
+
ConnectorHttpClient,
|
|
28
|
+
ConnectorHttpRequest,
|
|
29
|
+
ConnectorHttpResponse,
|
|
30
|
+
HttpMethod,
|
|
31
|
+
decodeJsonResponse
|
|
32
|
+
} from './http.ts'
|
|
33
|
+
export type { ConnectorHttpClientApi } from './http.ts'
|
|
34
|
+
export { ConnectorIntegration, IntegrationConfig, makeIntegration } from './integration.ts'
|
|
35
|
+
export { ActionResult, ProviderFailure } from './result.ts'
|
|
36
|
+
export type { ActionResult as ActionResultType } from './result.ts'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as Schema from 'effect/Schema'
|
|
2
|
+
import { CredentialBinding } from './credential.ts'
|
|
3
|
+
|
|
4
|
+
export const IntegrationConfig = Schema.Record(Schema.String, Schema.Unknown)
|
|
5
|
+
export type IntegrationConfig = typeof IntegrationConfig.Type
|
|
6
|
+
|
|
7
|
+
export class ConnectorIntegration extends Schema.Class<ConnectorIntegration>('ConnectorIntegration')({
|
|
8
|
+
id: Schema.optional(Schema.String),
|
|
9
|
+
connectorId: Schema.String,
|
|
10
|
+
config: IntegrationConfig,
|
|
11
|
+
credentialBindings: Schema.Array(CredentialBinding),
|
|
12
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
export const makeIntegration = (input: {
|
|
16
|
+
readonly id?: string
|
|
17
|
+
readonly connectorId: string
|
|
18
|
+
readonly config?: IntegrationConfig
|
|
19
|
+
readonly credentialBindings?: ReadonlyArray<CredentialBinding>
|
|
20
|
+
readonly metadata?: Readonly<Record<string, unknown>>
|
|
21
|
+
}) =>
|
|
22
|
+
ConnectorIntegration.make({
|
|
23
|
+
id: input.id,
|
|
24
|
+
connectorId: input.connectorId,
|
|
25
|
+
config: input.config ?? {},
|
|
26
|
+
credentialBindings: input.credentialBindings ?? [],
|
|
27
|
+
metadata: input.metadata
|
|
28
|
+
})
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import { defineAction } from '../action.ts'
|
|
4
|
+
import { defineConnector } from '../connector.ts'
|
|
5
|
+
import { CredentialSlot, resolveCredential } from '../credential.ts'
|
|
6
|
+
import { ConnectorError } from '../error.ts'
|
|
7
|
+
import { ConnectorHttpClient, ConnectorHttpRequest, decodeJsonResponse } from '../http.ts'
|
|
8
|
+
import { ActionResult, ProviderFailure } from '../result.ts'
|
|
9
|
+
import type { ConnectorIntegration } from '../integration.ts'
|
|
10
|
+
|
|
11
|
+
export const linkedInSearchConnectorId = 'linkedin-search'
|
|
12
|
+
export const exaApiKeySlotId = 'linkedin-search.exa_api_key'
|
|
13
|
+
export const enrichLayerApiKeySlotId = 'linkedin-search.enrich_layer_api_key'
|
|
14
|
+
export const exaApiBaseUrl = 'https://api.exa.ai'
|
|
15
|
+
export const enrichLayerApiBaseUrl = 'https://enrichlayer.com/api/v2'
|
|
16
|
+
|
|
17
|
+
export const ExaApiKeySlot = CredentialSlot.make({ id: exaApiKeySlotId, kind: 'api_key' })
|
|
18
|
+
export const EnrichLayerApiKeySlot = CredentialSlot.make({
|
|
19
|
+
id: enrichLayerApiKeySlotId,
|
|
20
|
+
kind: 'api_key'
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const resolveApiToken = (integration: ConnectorIntegration, slot: CredentialSlot) =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const credential = yield* resolveCredential(integration, slot)
|
|
26
|
+
|
|
27
|
+
switch (credential._tag) {
|
|
28
|
+
case 'ApiKeyCredential':
|
|
29
|
+
return credential.key
|
|
30
|
+
case 'BearerTokenCredential':
|
|
31
|
+
return credential.token
|
|
32
|
+
case 'OAuthCredential':
|
|
33
|
+
return credential.accessToken
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const isSuccessStatus = (status: number) => status >= 200 && status < 300
|
|
38
|
+
|
|
39
|
+
const linkedInProviderFailure = (input: {
|
|
40
|
+
readonly code: string
|
|
41
|
+
readonly message: string
|
|
42
|
+
readonly status: number
|
|
43
|
+
readonly body: string
|
|
44
|
+
}) =>
|
|
45
|
+
ActionResult.failure(
|
|
46
|
+
new ProviderFailure({
|
|
47
|
+
code: input.code,
|
|
48
|
+
message: input.message,
|
|
49
|
+
status: input.status,
|
|
50
|
+
underlying: input.body
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
export class LinkedInSearchInput extends Schema.Class<LinkedInSearchInput>('LinkedInSearchInput')({
|
|
55
|
+
query: Schema.String,
|
|
56
|
+
numResults: Schema.optional(Schema.Number)
|
|
57
|
+
}) {}
|
|
58
|
+
|
|
59
|
+
export const LinkedInSearchResult = Schema.Struct({
|
|
60
|
+
title: Schema.optional(Schema.String),
|
|
61
|
+
url: Schema.optional(Schema.String),
|
|
62
|
+
text: Schema.optional(Schema.String),
|
|
63
|
+
publishedDate: Schema.optional(Schema.String),
|
|
64
|
+
author: Schema.optional(Schema.String)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export class LinkedInSearchOutput extends Schema.Class<LinkedInSearchOutput>('LinkedInSearchOutput')({
|
|
68
|
+
results: Schema.Array(LinkedInSearchResult),
|
|
69
|
+
totalResults: Schema.optional(Schema.Number)
|
|
70
|
+
}) {}
|
|
71
|
+
|
|
72
|
+
export class LinkedInProfileInput extends Schema.Class<LinkedInProfileInput>('LinkedInProfileInput')({
|
|
73
|
+
linkedinUrl: Schema.String
|
|
74
|
+
}) {}
|
|
75
|
+
|
|
76
|
+
export class LinkedInProfileOutput extends Schema.Class<LinkedInProfileOutput>('LinkedInProfileOutput')({
|
|
77
|
+
profile: Schema.Unknown
|
|
78
|
+
}) {}
|
|
79
|
+
|
|
80
|
+
export class LinkedInEmailOutput extends Schema.Class<LinkedInEmailOutput>('LinkedInEmailOutput')({
|
|
81
|
+
email: Schema.NullOr(Schema.String),
|
|
82
|
+
status: Schema.optional(Schema.String),
|
|
83
|
+
message: Schema.optional(Schema.String)
|
|
84
|
+
}) {}
|
|
85
|
+
|
|
86
|
+
const missingEnrichLayer = (integration: ConnectorIntegration) =>
|
|
87
|
+
new ConnectorError({
|
|
88
|
+
cause: 'credential_binding_missing',
|
|
89
|
+
message: 'Missing Enrich Layer credential binding',
|
|
90
|
+
connectorId: integration.connectorId,
|
|
91
|
+
slotId: EnrichLayerApiKeySlot.id
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export const linkedInSearchAction = defineAction({
|
|
95
|
+
id: 'linkedin_search.search',
|
|
96
|
+
description: 'Search LinkedIn people results through Exa.',
|
|
97
|
+
inputSchema: LinkedInSearchInput,
|
|
98
|
+
outputSchema: LinkedInSearchOutput,
|
|
99
|
+
execute: ({ integration, input }) =>
|
|
100
|
+
Effect.gen(function* () {
|
|
101
|
+
const token = yield* resolveApiToken(integration, ExaApiKeySlot)
|
|
102
|
+
const http = yield* ConnectorHttpClient
|
|
103
|
+
const response = yield* http.request(
|
|
104
|
+
ConnectorHttpRequest.make({
|
|
105
|
+
method: 'POST',
|
|
106
|
+
url: `${exaApiBaseUrl}/search`,
|
|
107
|
+
headers: {
|
|
108
|
+
authorization: `Bearer ${token}`,
|
|
109
|
+
'content-type': 'application/json'
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
query: input.query,
|
|
113
|
+
category: 'people',
|
|
114
|
+
numResults: input.numResults ?? 10,
|
|
115
|
+
type: 'auto',
|
|
116
|
+
contents: { text: true }
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if (!isSuccessStatus(response.status)) {
|
|
122
|
+
return linkedInProviderFailure({
|
|
123
|
+
code: 'linkedin_search_failed',
|
|
124
|
+
message: 'LinkedIn search failed',
|
|
125
|
+
status: response.status,
|
|
126
|
+
body: response.body
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const decoded = yield* decodeJsonResponse(
|
|
131
|
+
Schema.Struct({ results: Schema.Array(LinkedInSearchResult), totalResults: Schema.optional(Schema.Number) }),
|
|
132
|
+
response
|
|
133
|
+
)
|
|
134
|
+
return ActionResult.success(LinkedInSearchOutput.make(decoded))
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
export const linkedInProfileAction = defineAction({
|
|
139
|
+
id: 'linkedin_search.profile',
|
|
140
|
+
description: 'Fetch a LinkedIn profile through Enrich Layer.',
|
|
141
|
+
inputSchema: LinkedInProfileInput,
|
|
142
|
+
outputSchema: LinkedInProfileOutput,
|
|
143
|
+
execute: ({ integration, input }) =>
|
|
144
|
+
Effect.gen(function* () {
|
|
145
|
+
const token = yield* resolveApiToken(integration, EnrichLayerApiKeySlot).pipe(
|
|
146
|
+
Effect.catchTag('ConnectorError', () => Effect.fail(missingEnrichLayer(integration)))
|
|
147
|
+
)
|
|
148
|
+
const http = yield* ConnectorHttpClient
|
|
149
|
+
const response = yield* http.request(
|
|
150
|
+
ConnectorHttpRequest.make({
|
|
151
|
+
method: 'GET',
|
|
152
|
+
url: `${enrichLayerApiBaseUrl}/profile?linkedin_profile_url=${encodeURIComponent(input.linkedinUrl)}`,
|
|
153
|
+
headers: { authorization: `Bearer ${token}` }
|
|
154
|
+
})
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if (!isSuccessStatus(response.status)) {
|
|
158
|
+
return linkedInProviderFailure({
|
|
159
|
+
code: 'linkedin_profile_failed',
|
|
160
|
+
message: 'LinkedIn profile fetch failed',
|
|
161
|
+
status: response.status,
|
|
162
|
+
body: response.body
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const profile = yield* decodeJsonResponse(Schema.Unknown, response)
|
|
167
|
+
return ActionResult.success(LinkedInProfileOutput.make({ profile }))
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
export const linkedInEmailAction = defineAction({
|
|
172
|
+
id: 'linkedin_search.email',
|
|
173
|
+
description: 'Fetch a LinkedIn profile email through Enrich Layer.',
|
|
174
|
+
inputSchema: LinkedInProfileInput,
|
|
175
|
+
outputSchema: LinkedInEmailOutput,
|
|
176
|
+
execute: ({ integration, input }) =>
|
|
177
|
+
Effect.gen(function* () {
|
|
178
|
+
const token = yield* resolveApiToken(integration, EnrichLayerApiKeySlot).pipe(
|
|
179
|
+
Effect.catchTag('ConnectorError', () => Effect.fail(missingEnrichLayer(integration)))
|
|
180
|
+
)
|
|
181
|
+
const http = yield* ConnectorHttpClient
|
|
182
|
+
const response = yield* http.request(
|
|
183
|
+
ConnectorHttpRequest.make({
|
|
184
|
+
method: 'GET',
|
|
185
|
+
url: `${enrichLayerApiBaseUrl}/profile/email?linkedin_profile_url=${encodeURIComponent(input.linkedinUrl)}`,
|
|
186
|
+
headers: { authorization: `Bearer ${token}` }
|
|
187
|
+
})
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if (!isSuccessStatus(response.status)) {
|
|
191
|
+
return linkedInProviderFailure({
|
|
192
|
+
code: 'linkedin_email_failed',
|
|
193
|
+
message: 'LinkedIn email fetch failed',
|
|
194
|
+
status: response.status,
|
|
195
|
+
body: response.body
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const decoded = yield* decodeJsonResponse(
|
|
200
|
+
Schema.Struct({
|
|
201
|
+
email: Schema.NullOr(Schema.String),
|
|
202
|
+
status: Schema.optional(Schema.String),
|
|
203
|
+
message: Schema.optional(Schema.String)
|
|
204
|
+
}),
|
|
205
|
+
response
|
|
206
|
+
)
|
|
207
|
+
return ActionResult.success(LinkedInEmailOutput.make(decoded))
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
export const linkedInSearchActions = [linkedInSearchAction, linkedInProfileAction, linkedInEmailAction]
|
|
212
|
+
|
|
213
|
+
export const LinkedInSearchConnector = defineConnector({
|
|
214
|
+
id: linkedInSearchConnectorId,
|
|
215
|
+
description: 'LinkedIn people search and enrichment connector actions.',
|
|
216
|
+
actions: linkedInSearchActions
|
|
217
|
+
})
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import { defineAction } from '../action.ts'
|
|
4
|
+
import { defineConnector } from '../connector.ts'
|
|
5
|
+
import { CredentialSlot, resolveCredential } from '../credential.ts'
|
|
6
|
+
import { ConnectorHttpClient, ConnectorHttpRequest, decodeJsonResponse } from '../http.ts'
|
|
7
|
+
import { ActionResult, ProviderFailure } from '../result.ts'
|
|
8
|
+
import type { ConnectorIntegration } from '../integration.ts'
|
|
9
|
+
|
|
10
|
+
export const notionConnectorId = 'notion'
|
|
11
|
+
export const notionApiTokenSlotId = 'notion.api_token'
|
|
12
|
+
export const notionApiBaseUrl = 'https://api.notion.com/v1'
|
|
13
|
+
export const notionVersion = '2022-06-28'
|
|
14
|
+
|
|
15
|
+
export const NotionApiTokenSlot = CredentialSlot.make({
|
|
16
|
+
id: notionApiTokenSlotId,
|
|
17
|
+
kind: 'api_key'
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const notionAuthorizationHeaders = (token: string) => ({
|
|
21
|
+
authorization: `Bearer ${token}`,
|
|
22
|
+
'notion-version': notionVersion
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const isSuccessStatus = (status: number) => status >= 200 && status < 300
|
|
26
|
+
|
|
27
|
+
const notionProviderFailure = (input: {
|
|
28
|
+
readonly code: string
|
|
29
|
+
readonly message: string
|
|
30
|
+
readonly status: number
|
|
31
|
+
readonly body: string
|
|
32
|
+
}) =>
|
|
33
|
+
ActionResult.failure(
|
|
34
|
+
new ProviderFailure({
|
|
35
|
+
code: input.code,
|
|
36
|
+
message: input.message,
|
|
37
|
+
status: input.status,
|
|
38
|
+
underlying: input.body
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const resolveNotionToken = (integration: ConnectorIntegration) =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const credential = yield* resolveCredential(integration, NotionApiTokenSlot)
|
|
45
|
+
|
|
46
|
+
switch (credential._tag) {
|
|
47
|
+
case 'ApiKeyCredential':
|
|
48
|
+
return credential.key
|
|
49
|
+
case 'BearerTokenCredential':
|
|
50
|
+
return credential.token
|
|
51
|
+
case 'OAuthCredential':
|
|
52
|
+
return credential.accessToken
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export const NotionRichText = Schema.Struct({
|
|
57
|
+
type: Schema.optional(Schema.String),
|
|
58
|
+
plain_text: Schema.optional(Schema.String),
|
|
59
|
+
href: Schema.optional(Schema.NullOr(Schema.String))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export const NotionTitleProperty = Schema.Struct({
|
|
63
|
+
title: Schema.Array(NotionRichText)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const NotionProperties = Schema.Record(Schema.String, Schema.Unknown)
|
|
67
|
+
|
|
68
|
+
export class NotionPage extends Schema.Class<NotionPage>('NotionPage')({
|
|
69
|
+
id: Schema.String,
|
|
70
|
+
object: Schema.String,
|
|
71
|
+
url: Schema.optional(Schema.String),
|
|
72
|
+
archived: Schema.optional(Schema.Boolean),
|
|
73
|
+
properties: Schema.optional(NotionProperties)
|
|
74
|
+
}) {}
|
|
75
|
+
|
|
76
|
+
export class NotionSearchInput extends Schema.Class<NotionSearchInput>('NotionSearchInput')({
|
|
77
|
+
query: Schema.optional(Schema.String),
|
|
78
|
+
pageSize: Schema.optional(Schema.Number),
|
|
79
|
+
startCursor: Schema.optional(Schema.String)
|
|
80
|
+
}) {}
|
|
81
|
+
|
|
82
|
+
export class NotionSearchOutput extends Schema.Class<NotionSearchOutput>('NotionSearchOutput')({
|
|
83
|
+
results: Schema.Array(Schema.Unknown),
|
|
84
|
+
nextCursor: Schema.optional(Schema.NullOr(Schema.String)),
|
|
85
|
+
hasMore: Schema.Boolean
|
|
86
|
+
}) {}
|
|
87
|
+
|
|
88
|
+
export class NotionGetPageInput extends Schema.Class<NotionGetPageInput>('NotionGetPageInput')({
|
|
89
|
+
pageId: Schema.String
|
|
90
|
+
}) {}
|
|
91
|
+
|
|
92
|
+
export class NotionCreatePageInput extends Schema.Class<NotionCreatePageInput>('NotionCreatePageInput')({
|
|
93
|
+
parentPageId: Schema.optional(Schema.String),
|
|
94
|
+
parentDatabaseId: Schema.optional(Schema.String),
|
|
95
|
+
title: Schema.String,
|
|
96
|
+
properties: Schema.optional(NotionProperties)
|
|
97
|
+
}) {}
|
|
98
|
+
|
|
99
|
+
const pageParent = (input: NotionCreatePageInput) => {
|
|
100
|
+
if (input.parentDatabaseId !== undefined) {
|
|
101
|
+
return { database_id: input.parentDatabaseId }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { page_id: input.parentPageId ?? '' }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const pageProperties = (input: NotionCreatePageInput) => ({
|
|
108
|
+
...input.properties,
|
|
109
|
+
title: {
|
|
110
|
+
title: [
|
|
111
|
+
{
|
|
112
|
+
text: {
|
|
113
|
+
content: input.title
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
export const notionSearchAction = defineAction({
|
|
121
|
+
id: 'notion.search',
|
|
122
|
+
description: 'Search pages and databases available to the Notion integration.',
|
|
123
|
+
inputSchema: NotionSearchInput,
|
|
124
|
+
outputSchema: NotionSearchOutput,
|
|
125
|
+
execute: ({ integration, input }) =>
|
|
126
|
+
Effect.gen(function* () {
|
|
127
|
+
const token = yield* resolveNotionToken(integration)
|
|
128
|
+
const http = yield* ConnectorHttpClient
|
|
129
|
+
const response = yield* http.request(
|
|
130
|
+
ConnectorHttpRequest.make({
|
|
131
|
+
method: 'POST',
|
|
132
|
+
url: `${notionApiBaseUrl}/search`,
|
|
133
|
+
headers: {
|
|
134
|
+
...notionAuthorizationHeaders(token),
|
|
135
|
+
'content-type': 'application/json'
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
query: input.query,
|
|
139
|
+
page_size: input.pageSize,
|
|
140
|
+
start_cursor: input.startCursor
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if (!isSuccessStatus(response.status)) {
|
|
146
|
+
return notionProviderFailure({
|
|
147
|
+
code: 'notion_search_failed',
|
|
148
|
+
message: 'Notion search failed',
|
|
149
|
+
status: response.status,
|
|
150
|
+
body: response.body
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const output = yield* decodeJsonResponse(NotionSearchOutput, response)
|
|
155
|
+
return ActionResult.success(output)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
export const notionGetPageAction = defineAction({
|
|
160
|
+
id: 'notion.get_page',
|
|
161
|
+
description: 'Get a Notion page by id.',
|
|
162
|
+
inputSchema: NotionGetPageInput,
|
|
163
|
+
outputSchema: NotionPage,
|
|
164
|
+
execute: ({ integration, input }) =>
|
|
165
|
+
Effect.gen(function* () {
|
|
166
|
+
const token = yield* resolveNotionToken(integration)
|
|
167
|
+
const http = yield* ConnectorHttpClient
|
|
168
|
+
const response = yield* http.request(
|
|
169
|
+
ConnectorHttpRequest.make({
|
|
170
|
+
method: 'GET',
|
|
171
|
+
url: `${notionApiBaseUrl}/pages/${encodeURIComponent(input.pageId)}`,
|
|
172
|
+
headers: notionAuthorizationHeaders(token)
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if (!isSuccessStatus(response.status)) {
|
|
177
|
+
return notionProviderFailure({
|
|
178
|
+
code: 'notion_get_page_failed',
|
|
179
|
+
message: 'Notion get page failed',
|
|
180
|
+
status: response.status,
|
|
181
|
+
body: response.body
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const output = yield* decodeJsonResponse(NotionPage, response)
|
|
186
|
+
return ActionResult.success(output)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
export const notionCreatePageAction = defineAction({
|
|
191
|
+
id: 'notion.create_page',
|
|
192
|
+
description: 'Create a Notion page under a parent page or database.',
|
|
193
|
+
inputSchema: NotionCreatePageInput,
|
|
194
|
+
outputSchema: NotionPage,
|
|
195
|
+
execute: ({ integration, input }) =>
|
|
196
|
+
Effect.gen(function* () {
|
|
197
|
+
const token = yield* resolveNotionToken(integration)
|
|
198
|
+
const http = yield* ConnectorHttpClient
|
|
199
|
+
const response = yield* http.request(
|
|
200
|
+
ConnectorHttpRequest.make({
|
|
201
|
+
method: 'POST',
|
|
202
|
+
url: `${notionApiBaseUrl}/pages`,
|
|
203
|
+
headers: {
|
|
204
|
+
...notionAuthorizationHeaders(token),
|
|
205
|
+
'content-type': 'application/json'
|
|
206
|
+
},
|
|
207
|
+
body: JSON.stringify({
|
|
208
|
+
parent: pageParent(input),
|
|
209
|
+
properties: pageProperties(input)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if (!isSuccessStatus(response.status)) {
|
|
215
|
+
return notionProviderFailure({
|
|
216
|
+
code: 'notion_create_page_failed',
|
|
217
|
+
message: 'Notion create page failed',
|
|
218
|
+
status: response.status,
|
|
219
|
+
body: response.body
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const output = yield* decodeJsonResponse(NotionPage, response)
|
|
224
|
+
return ActionResult.success(output)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
export const notionActions = [notionSearchAction, notionGetPageAction, notionCreatePageAction]
|
|
229
|
+
|
|
230
|
+
export const NotionConnector = defineConnector({
|
|
231
|
+
id: notionConnectorId,
|
|
232
|
+
description: 'Notion page and search connector actions.',
|
|
233
|
+
actions: notionActions
|
|
234
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Context, Effect } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import { defineAction } from '../action.ts'
|
|
4
|
+
import { requiredStringConfig } from '../config.ts'
|
|
5
|
+
import { defineConnector } from '../connector.ts'
|
|
6
|
+
import { CredentialSlot, resolveCredential } from '../credential.ts'
|
|
7
|
+
import { ActionResult } from '../result.ts'
|
|
8
|
+
import type { ConnectorError } from '../error.ts'
|
|
9
|
+
import type { ConnectorIntegration } from '../integration.ts'
|
|
10
|
+
|
|
11
|
+
export const r2StorageConnectorId = 'r2-storage'
|
|
12
|
+
export const r2AccessKeyIdSlotId = 'r2-storage.access_key_id'
|
|
13
|
+
export const r2SecretAccessKeySlotId = 'r2-storage.secret_access_key'
|
|
14
|
+
|
|
15
|
+
export const R2AccessKeyIdSlot = CredentialSlot.make({ id: r2AccessKeyIdSlotId, kind: 'api_key' })
|
|
16
|
+
export const R2SecretAccessKeySlot = CredentialSlot.make({
|
|
17
|
+
id: r2SecretAccessKeySlotId,
|
|
18
|
+
kind: 'api_key'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export class R2PresignInput extends Schema.Class<R2PresignInput>('R2PresignInput')({
|
|
22
|
+
endpoint: Schema.String,
|
|
23
|
+
accessKeyId: Schema.String,
|
|
24
|
+
secretAccessKey: Schema.String,
|
|
25
|
+
bucket: Schema.String,
|
|
26
|
+
key: Schema.String,
|
|
27
|
+
contentType: Schema.String
|
|
28
|
+
}) {}
|
|
29
|
+
|
|
30
|
+
export class R2PresignOutput extends Schema.Class<R2PresignOutput>('R2PresignOutput')({
|
|
31
|
+
uploadUrl: Schema.String
|
|
32
|
+
}) {}
|
|
33
|
+
|
|
34
|
+
export type R2PresignerApi = {
|
|
35
|
+
readonly presignPutObject: (input: R2PresignInput) => Effect.Effect<R2PresignOutput, ConnectorError>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class R2Presigner extends Context.Service<R2Presigner, R2PresignerApi>()(
|
|
39
|
+
'@yolk-sdk/connectors/R2Presigner'
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
const resolveApiToken = (integration: ConnectorIntegration, slot: CredentialSlot) =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const credential = yield* resolveCredential(integration, slot)
|
|
45
|
+
|
|
46
|
+
switch (credential._tag) {
|
|
47
|
+
case 'ApiKeyCredential':
|
|
48
|
+
return credential.key
|
|
49
|
+
case 'BearerTokenCredential':
|
|
50
|
+
return credential.token
|
|
51
|
+
case 'OAuthCredential':
|
|
52
|
+
return credential.accessToken
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const joinPublicUrl = (publicUrl: string, key: string) => {
|
|
57
|
+
const base = publicUrl.endsWith('/') ? publicUrl.slice(0, -1) : publicUrl
|
|
58
|
+
return `${base}/${key}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const safeObjectKey = (filename: string) => {
|
|
62
|
+
const trimmed = filename.trim().replace(/^\/+/, '')
|
|
63
|
+
return trimmed === '' ? `uploads/${Date.now()}` : trimmed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class R2UploadUrlInput extends Schema.Class<R2UploadUrlInput>('R2UploadUrlInput')({
|
|
67
|
+
filename: Schema.String,
|
|
68
|
+
contentType: Schema.String
|
|
69
|
+
}) {}
|
|
70
|
+
|
|
71
|
+
export class R2UploadUrlOutput extends Schema.Class<R2UploadUrlOutput>('R2UploadUrlOutput')({
|
|
72
|
+
uploadUrl: Schema.String,
|
|
73
|
+
publicUrl: Schema.String,
|
|
74
|
+
key: Schema.String
|
|
75
|
+
}) {}
|
|
76
|
+
|
|
77
|
+
export const r2StorageUploadUrlAction = defineAction({
|
|
78
|
+
id: 'r2_storage.upload_url',
|
|
79
|
+
description: 'Create a presigned R2 PUT upload URL.',
|
|
80
|
+
inputSchema: R2UploadUrlInput,
|
|
81
|
+
outputSchema: R2UploadUrlOutput,
|
|
82
|
+
execute: ({ integration, input }) =>
|
|
83
|
+
Effect.gen(function* () {
|
|
84
|
+
const endpoint = yield* requiredStringConfig(integration, 'endpoint')
|
|
85
|
+
const bucket = yield* requiredStringConfig(integration, 'bucket')
|
|
86
|
+
const publicUrl = yield* requiredStringConfig(integration, 'publicUrl')
|
|
87
|
+
const accessKeyId = yield* resolveApiToken(integration, R2AccessKeyIdSlot)
|
|
88
|
+
const secretAccessKey = yield* resolveApiToken(integration, R2SecretAccessKeySlot)
|
|
89
|
+
const presigner = yield* R2Presigner
|
|
90
|
+
const key = safeObjectKey(input.filename)
|
|
91
|
+
const presigned = yield* presigner.presignPutObject(
|
|
92
|
+
R2PresignInput.make({
|
|
93
|
+
endpoint,
|
|
94
|
+
accessKeyId,
|
|
95
|
+
secretAccessKey,
|
|
96
|
+
bucket,
|
|
97
|
+
key,
|
|
98
|
+
contentType: input.contentType
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return ActionResult.success(
|
|
103
|
+
R2UploadUrlOutput.make({
|
|
104
|
+
uploadUrl: presigned.uploadUrl,
|
|
105
|
+
publicUrl: joinPublicUrl(publicUrl, key),
|
|
106
|
+
key
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
export const r2StorageActions = [r2StorageUploadUrlAction]
|
|
113
|
+
|
|
114
|
+
export const R2StorageConnector = defineConnector({
|
|
115
|
+
id: r2StorageConnectorId,
|
|
116
|
+
description: 'Cloudflare R2 storage connector actions.',
|
|
117
|
+
actions: r2StorageActions
|
|
118
|
+
})
|