Skip to content

Commit

Permalink
Merge branch 'main' into input-url
Browse files Browse the repository at this point in the history
  • Loading branch information
danphilibin authored Jul 29, 2022
2 parents 87db9af + d4340c8 commit 97d8573
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 47 deletions.
8 changes: 7 additions & 1 deletion envoy.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { EnvoyVariableSpec } from '@interval/envoy'

const vars: EnvoyVariableSpec[] = ['DEMO_API_KEY']
const vars: EnvoyVariableSpec[] = [
'DEMO_API_KEY',
{ name: 'AWS_KEY_ID', isRequired: false },
{ name: 'AWS_KEY_SECRET', isRequired: false },
{ name: 'AWS_S3_IO_BUCKET', isRequired: false },
{ name: 'AWS_REGION', isRequired: false },
]

export default vars
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"zod": "^3.13.3"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.135.0",
"@aws-sdk/s3-request-presigner": "^3.135.0",
"@faker-js/faker": "^7.3.0",
"@interval/envoy": "^1.0.1",
"@types/dedent": "^0.7.0",
Expand Down
14 changes: 8 additions & 6 deletions src/classes/IOClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,11 @@ export class IOClient {
url: this.createIOMethod('INPUT_URL', {
componentDef: urlInput,
}),
date: this.createIOMethod('INPUT_DATE', { componentDef: date }),
time: this.createIOMethod('INPUT_TIME'),
datetime: this.createIOMethod('INPUT_DATETIME', {
componentDef: datetime,
}),
},
select: {
single: this.createIOMethod('SELECT_SINGLE', {
Expand Down Expand Up @@ -453,13 +458,10 @@ export class IOClient {
propsRequired: true,
componentDef: spreadsheet,
}),
date: this.createIOMethod('INPUT_DATE', { componentDef: date }),
time: this.createIOMethod('INPUT_TIME'),
datetime: this.createIOMethod('INPUT_DATETIME', {
componentDef: datetime,
}),
input: {
file: this.createIOMethod('UPLOAD_FILE', { componentDef: file }),
file: this.createIOMethod('UPLOAD_FILE', {
componentDef: file(this.logger),
}),
},
},
}
Expand Down
4 changes: 0 additions & 4 deletions src/classes/IOComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,6 @@ export default class IOComponent<MethodName extends T_IO_METHOD_NAMES> {
return this.instance
}

setOptional(optional: boolean) {
this.instance.isOptional = optional
}

get label() {
return this.instance.label
}
Expand Down
3 changes: 3 additions & 0 deletions src/components/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ export default function search<Result = any>({
onSearch,
initialResults = [],
renderResult,
disabled = false,
...rest
}: {
placeholder?: string
helpText?: string
disabled?: boolean
initialResults?: Result[]
renderResult: (result: Result) => RenderResultDef
onSearch: (query: string) => Promise<Result[]>
Expand Down Expand Up @@ -58,6 +60,7 @@ export default function search<Result = any>({
const props: T_IO_PROPS<'SEARCH'> = {
...rest,
results: renderResults(initialResults),
disabled,
}

return {
Expand Down
89 changes: 60 additions & 29 deletions src/components/upload.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,8 @@
import path from 'path'
import fetch, { Response } from 'node-fetch'
import { IntervalError } from '..'
import { T_IO_PROPS, T_IO_RETURNS } from '../ioSchema'

export function file(_: T_IO_PROPS<'UPLOAD_FILE'>) {
return {
getValue({ url, ...response }: T_IO_RETURNS<'UPLOAD_FILE'>) {
return {
...response,
lastModified: new Date(response.lastModified),
get extension(): string {
return path.extname(response.name)
},
async url(): Promise<string> {
return url
},
async text(): Promise<string> {
return retryFetch(url).then(r => r.text())
},
async json(): Promise<any> {
return retryFetch(url).then(r => r.json())
},
async buffer(): Promise<Buffer> {
return retryFetch(url)
.then(r => r.arrayBuffer())
.then(arrayBuffer => Buffer.from(arrayBuffer))
},
}
},
}
}
import { T_IO_PROPS, T_IO_RETURNS, T_IO_STATE } from '../ioSchema'
import Logger from '../classes/Logger'

const MAX_RETRIES = 3

Expand All @@ -48,3 +21,61 @@ async function retryFetch(url: string): Promise<Response> {
// This should never happen, final failing response err would be thrown above
throw new IntervalError('Failed to fetch file.')
}

type UploaderProps = T_IO_PROPS<'UPLOAD_FILE'> & {
generatePresignedUrls?: (
state: T_IO_STATE<'UPLOAD_FILE'>
) => Promise<{ uploadUrl: string; downloadUrl: string }>
}

export function file(logger: Logger) {
return function ({ generatePresignedUrls, ...props }: UploaderProps) {
const isProvidingUrls = !!generatePresignedUrls
return {
props: {
...props,
uploadUrl: isProvidingUrls ? null : undefined,
downloadUrl: isProvidingUrls ? null : undefined,
},
getValue({ url, ...response }: T_IO_RETURNS<'UPLOAD_FILE'>) {
return {
...response,
lastModified: new Date(response.lastModified),
get extension(): string {
return path.extname(response.name)
},
async url(): Promise<string> {
return url
},
async text(): Promise<string> {
return retryFetch(url).then(r => r.text())
},
async json(): Promise<any> {
return retryFetch(url).then(r => r.json())
},
async buffer(): Promise<Buffer> {
return retryFetch(url)
.then(r => r.arrayBuffer())
.then(arrayBuffer => Buffer.from(arrayBuffer))
},
}
},
async onStateChange(newState: T_IO_STATE<'UPLOAD_FILE'>) {
if (!generatePresignedUrls) {
return { uploadUrl: undefined, downloadUrl: undefined }
}

try {
const urls = await generatePresignedUrls(newState)
return urls
} catch (error) {
logger.error(
'An error was unexpectedly thrown from the `generatePresignedUrls` function:'
)
logger.error(error)
return { uploadUrl: 'error', downloadUrl: 'error' }
}
},
}
}
}
94 changes: 88 additions & 6 deletions src/examples/basic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from './selectFromTable'
import unauthorized from './unauthorized'
import './ghostHost'
import { generateS3Urls } from '../utils/upload'

const actionLinks: IntervalActionHandler = async () => {
await io.group([
Expand Down Expand Up @@ -127,6 +128,70 @@ const interval = new Interval({
logLevel: 'debug',
endpoint: 'ws://localhost:3000/websocket',
actions: {
disabled_inputs: async io => {
await io.group([
io.display.heading('Here are a bunch of disabled inputs'),
io.input.text('Text input', {
disabled: true,
placeholder: 'Text goes here',
}),
io.experimental.datetime('Date & time', { disabled: true }),
io.input.boolean('Boolean input', { disabled: true }),
io.select.single('Select something', {
options: [1, 2, 3],
disabled: true,
}),
io.input.number('Number input', {
disabled: true,
}),
io.input.email('Email input', { disabled: true }),
io.input.richText('Rich text input', { disabled: true }),
io.search('Search for a user', {
disabled: true,
renderResult: user => ({
label: user.name,
description: user.email,
}),
onSearch: async query => {
return [
{
name: 'John Doe',
email: '[email protected]',
},
]
},
}),
io.select.multiple('Select multiple of something', {
options: [1, 2, 3],
disabled: true,
}),
io.select.table('Select from table', {
data: [
{
album: 'Exile on Main Street',
artist: 'The Rolling Stones',
year: 1972,
},
{
artist: 'Michael Jackson',
album: 'Thriller',
year: 1982,
},
{
album: 'Enter the Wu-Tang (36 Chambers)',
artist: 'Wu-Tang Clan',
year: 1993,
},
],
disabled: true,
}),
io.experimental.date('Date input', { disabled: true }),
io.experimental.time('Time input', { disabled: true }),
io.experimental.input.file('File input', { disabled: true }),
])

return 'Done!'
},
'long-return-string': async io => {
return {
date: new Date(),
Expand Down Expand Up @@ -312,7 +377,7 @@ const interval = new Interval({
},
dates: async io => {
const [date, time, datetime] = await io.group([
io.experimental.date('Enter a date', {
io.input.date('Enter a date', {
min: {
year: 2020,
month: 1,
Expand All @@ -324,7 +389,7 @@ const interval = new Interval({
day: 30,
},
}),
io.experimental.time('Enter a time', {
io.input.time('Enter a time', {
min: {
hour: 8,
minute: 30,
Expand All @@ -334,7 +399,7 @@ const interval = new Interval({
minute: 0,
},
}),
io.experimental.datetime('Enter a datetime', {
io.input.datetime('Enter a datetime', {
defaultValue: new Date(),
min: new Date(),
}),
Expand Down Expand Up @@ -676,10 +741,27 @@ const interval = new Interval({

return { message: 'OK, notified!' }
},
upload: async io => {
upload: async (io, ctx) => {
const customDestinationFile = await io.experimental.input.file(
'Upload an image!',
{
helpText: 'Will be uploaded to the custom destination.',
allowedExtensions: ['.gif', '.jpg', '.jpeg', '.png'],
generatePresignedUrls: async ({ name }) => {
const urlSafeName = name.replace(/ /g, '-')
const path = `custom-endpoint/${new Date().getTime()}-${urlSafeName}`

return generateS3Urls(path)
},
}
)

console.log(await customDestinationFile.url())

const file = await io.experimental.input.file('Upload an image!', {
helpText: 'Can be any image, or a CSV (?).',
allowedExtensions: ['.gif', '.jpg', '.jpeg', 'csv'],
helpText:
'Will be uploaded to Interval and expire after the action finishes running.',
allowedExtensions: ['.gif', '.jpg', '.jpeg', '.png'],
})

console.log(file)
Expand Down
41 changes: 41 additions & 0 deletions src/examples/utils/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
AWS_REGION,
AWS_KEY_ID,
AWS_KEY_SECRET,
AWS_S3_IO_BUCKET,
} from '../../env'
import {
GetObjectCommand,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import * as urlParser from '@aws-sdk/url-parser'

export async function generateS3Urls(key: string) {
if (!AWS_KEY_ID || !AWS_KEY_SECRET || !AWS_S3_IO_BUCKET) {
throw new Error('Missing AWS credentials')
}

const s3Client = new S3Client({
region: AWS_REGION ?? 'us-west-1',
credentials: {
accessKeyId: AWS_KEY_ID,
secretAccessKey: AWS_KEY_SECRET,
},
})

const command = new PutObjectCommand({
Bucket: AWS_S3_IO_BUCKET,
Key: key,
})

const uploadUrl = await getSignedUrl(s3Client, command, {
expiresIn: 3600, // 1 hour
})

const url = new URL(uploadUrl)
const downloadUrl = url.origin + url.pathname

return { uploadUrl, downloadUrl }
}
Loading

0 comments on commit 97d8573

Please sign in to comment.