Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use fetch for api handler #530

Merged
merged 5 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 44 additions & 25 deletions api/fake-seam-connect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { Readable, type Stream } from 'node:stream'
import type { Readable } from 'node:stream'

import {
createFake as createFakeDevicedb,
Expand All @@ -10,8 +10,6 @@ import {
} from '@seamapi/fake-devicedb'
import { createFake } from '@seamapi/fake-seam-connect'
import type { VercelRequest, VercelResponse } from '@vercel/node'
import axios from 'axios'
import getRawBody from 'raw-body'

// eslint-disable-next-line import/no-relative-parent-imports
import { seedFake } from '../.storybook/seed-fake.js'
Expand Down Expand Up @@ -40,7 +38,8 @@ export default async (
req: VercelRequest,
res: VercelResponse
): Promise<void> => {
const { apipath, ...getParams } = req.query
const { method } = req
const { apipath, ...query } = req.query

const fake = await createFake()
seedFake(fake.database)
Expand All @@ -58,28 +57,47 @@ export default async (
if (host == null) throw new Error('Missing Host header')
await fake.startServer({ baseUrl: `https://${host}/api` })

const requestBuffer = await getRawBody(req)
const body = await getArrayBufferFromReadableStream(req)

if (typeof apipath !== 'string') {
throw new Error('Expected apipath to be a string')
}

const { status, data, headers } = await axios.request({
url: `${fake.serverUrl}/${apipath}`,
params: getParams,
method: req.method,
headers: { ...req.headers },
data: getRequestStreamFromBuffer(requestBuffer),
timeout: 10_000,
validateStatus: () => true,
maxRedirects: 0,
responseType: 'arraybuffer',
const serverUrl = fake.serverUrl
if (serverUrl == null) {
throw new Error('Fake serverUrl was null')
}

if (method == null) {
throw new Error('Request method undefined')
}

const url = new URL(apipath, serverUrl)
for (const [k, v] of Object.entries(query)) {
if (typeof v === 'string') url.searchParams.append(k, v)
}

const reqHeaders: Record<string, string> = {}
for (const [k, v] of Object.entries(req.headers)) {
if (k === 'content-length') continue
if (typeof v === 'string') reqHeaders[k] = v
}
const proxyRes = await fetch(url, {
redirect: 'follow',
mode: 'cors',
credentials: 'include',
method,
headers: reqHeaders,
...(['GET', 'HEAD', 'OPTIONS'].includes(method) ? {} : { body }),
})

const { status, headers } = proxyRes
const data = await proxyRes.arrayBuffer()

res.status(status)

for (const [key, value] of Object.entries(headers)) {
if (!unproxiedHeaders.has(key)) res.setHeader(key, value as string)
for (const [key, value] of headers) {
if (!unproxiedHeaders.has(key)) res.setHeader(key, value)
}

res.end(Buffer.from(data as Buffer))
Expand All @@ -95,12 +113,13 @@ const getFakeDevicedb = async (): Promise<FakeDevicedb> => {
return fake
}

// https://stackoverflow.com/a/44091532/559475
const getRequestStreamFromBuffer = (requestBuffer: Buffer): Stream => {
const requestStream = new Readable()
// eslint-disable-next-line @typescript-eslint/no-empty-function
requestStream._read = () => {}
requestStream.push(requestBuffer)
requestStream.push(null)
return requestStream
const getArrayBufferFromReadableStream = async (
readable: Readable
): Promise<ArrayBuffer> => {
const chunks = []
for await (const chunk of readable) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
}
const buf = Buffer.concat(chunks)
return new Uint8Array(buf).buffer
}
2 changes: 0 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@
"@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-v8": "^0.34.4",
"@vitest/ui": "^0.34.4",
"axios": "^1.4.0",
"concurrently": "^8.0.1",
"copy-webpack-plugin": "^11.0.0",
"del-cli": "^5.0.0",
Expand All @@ -182,7 +181,6 @@
"happy-dom": "^12.10.3",
"http-proxy-middleware": "^2.0.6",
"prettier": "^3.0.0",
"raw-body": "^2.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.62.1",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/telemetry/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class TelemetryClient {
}: TelemetryClientOptions = {}) {
this.#queue = new Queue()
this.#anonymousId = uuidv4()
this.#endpoint = endpoint
this.#endpoint = endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint
this.#debug = debug
this.#disabled = disabled
}
Expand Down
28 changes: 17 additions & 11 deletions src/lib/ui/ClimateSettingForm/ClimateSettingScheduleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from 'seamapi'

import { getSystemTimeZone } from 'lib/dates.js'
import { useDevice } from 'lib/index.js'
import { useSeamClient } from 'lib/index.js'
import { ClimateSettingScheduleFormClimateSetting } from 'lib/ui/ClimateSettingForm/ClimateSettingScheduleFormClimateSetting.js'
import { ClimateSettingScheduleFormDefaultClimateSetting } from 'lib/ui/ClimateSettingForm/ClimateSettingScheduleFormDefaultClimateSetting.js'
import { ClimateSettingScheduleFormDeviceSelect } from 'lib/ui/ClimateSettingForm/ClimateSettingScheduleFormDeviceSelect.js'
Expand Down Expand Up @@ -61,6 +61,7 @@ export function ClimateSettingScheduleForm({
function Content({
onBack,
}: Omit<ClimateSettingScheduleFormProps, 'className'>): JSX.Element {
const { client } = useSeamClient()
const { control, watch, resetField } =
useForm<ClimateSettingScheduleFormFields>({
defaultValues: {
Expand All @@ -80,10 +81,6 @@ function Content({
const deviceId = watch('deviceId')
const timeZone = watch('timeZone')

const { device } = useDevice({
device_id: deviceId,
})

const [page, setPage] = useState<
| 'device_select'
| 'default_setting'
Expand All @@ -98,13 +95,22 @@ function Content({
}

useEffect(() => {
if (page === 'device_select' && device != null) {
if (!isThermostatDevice(device)) return
const defaultSetting = device.properties.default_climate_setting
if (defaultSetting != null) setPage('name_and_schedule')
else setPage('default_setting')
if (page === 'device_select' && deviceId !== '' && client != null) {
client.devices
.get({ device_id: deviceId })
.then((device) => {
if (!isThermostatDevice(device)) return

if (device.properties.default_climate_setting != null) {
setPage('name_and_schedule')
return
}

setPage('default_setting')
})
.catch(() => {})
}
}, [device, page, setPage])
}, [client, deviceId, page, setPage])

if (page === 'device_select') {
return (
Expand Down
Loading