diff --git a/package.json b/package.json index 54aee9c..216444f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "types": "dist/index.d.ts", "scripts": { "tar": "yarn pack", + "check": "tsc --noEmit", "build:envoy": "yarn envoy $INIT_CWD/envoy.config.ts $INIT_CWD/.env $INIT_CWD/src/env.ts", "build": "yarn build:envoy && tsc", "demo:basic": "node ./dist/examples/basic/index.js", diff --git a/src/classes/IOClient.ts b/src/classes/IOClient.ts index c9acb81..fcde536 100644 --- a/src/classes/IOClient.ts +++ b/src/classes/IOClient.ts @@ -152,87 +152,108 @@ export class IOClient { } this.onResponseHandler = async result => { - if (result.inputGroupKey && result.inputGroupKey !== inputGroupKey) { - this.logger.debug('Received response for other input group') - return - } - - if (this.isCanceled || isReturned) { - this.logger.debug('Received response after IO call complete') - return - } + try { + if ( + result.inputGroupKey && + result.inputGroupKey !== inputGroupKey + ) { + this.logger.debug('Received response for other input group') + return + } - // Transaction canceled from Interval cloud UI - if (result.kind === 'CANCELED') { - this.isCanceled = true - reject(new IOError('CANCELED')) - return - } + if (this.isCanceled || isReturned) { + this.logger.debug('Received response after IO call complete') + return + } - if (result.values.length !== components.length) { - throw new Error('Mismatch in return array length') - } + // Transaction canceled from Interval cloud UI + if (result.kind === 'CANCELED') { + this.isCanceled = true + reject(new IOError('CANCELED')) + return + } - if (result.valuesMeta) { - result.values = superjson.deserialize({ - json: result.values, - meta: result.valuesMeta, - }) - } + if (result.values.length !== components.length) { + throw new Error('Mismatch in return array length') + } - if (result.kind === 'RETURN') { - const validities = await Promise.all( - result.values.map(async (v, index) => { - const component = components[index] - if (component.validator) { - const resp = await component.handleValidation(v) - if (resp !== undefined) { - return false - } - } - return true + if (result.valuesMeta) { + result.values = superjson.deserialize({ + json: result.values, + meta: result.valuesMeta, }) - ) - - validationErrorMessage = undefined - - if (validities.some(v => !v)) { - render() - return } - if (groupValidator) { - validationErrorMessage = await groupValidator( - result.values as IOClientRenderReturnValues + if (result.kind === 'RETURN') { + const validities = await Promise.all( + result.values.map(async (v, index) => { + const component = components[index] + if (component.validator) { + const resp = await component.handleValidation(v) + if (resp !== undefined) { + return false + } + } + return true + }) ) - if (validationErrorMessage) { + validationErrorMessage = undefined + + if (validities.some(v => !v)) { render() return } - } - isReturned = true - - result.values.forEach((v, index) => { - // @ts-ignore - components[index].setReturnValue(v) - }) + if (groupValidator) { + validationErrorMessage = await groupValidator( + result.values as IOClientRenderReturnValues + ) - return - } + if (validationErrorMessage) { + render() + return + } + } - if (result.kind === 'SET_STATE') { - for (const [index, newState] of result.values.entries()) { - const prevState = components[index].getInstance().state + isReturned = true - if (JSON.stringify(newState) !== JSON.stringify(prevState)) { - this.logger.debug(`New state at ${index}`, newState) + result.values.forEach((v, index) => { // @ts-ignore - await components[index].setState(newState) + components[index].setReturnValue(v) + }) + + return + } + + if (result.kind === 'SET_STATE') { + for (const [index, newState] of result.values.entries()) { + const prevState = components[index].getInstance().state + + if (JSON.stringify(newState) !== JSON.stringify(prevState)) { + this.logger.debug(`New state at ${index}`, newState) + // @ts-ignore + await components[index].setState(newState) + } + } + render() + } + } catch (err) { + if (err instanceof Error) { + const errorCause = err.cause + ? err.cause instanceof Error + ? err.cause.message + : err.cause + : undefined + if (errorCause) { + this.logger.error(`${err.message}:`, errorCause) + } else { + this.logger.error(err.message) } + } else { + this.logger.error(err) } - render() + reject(err) } } diff --git a/src/classes/IOComponent.ts b/src/classes/IOComponent.ts index 4b60a3a..753057c 100644 --- a/src/classes/IOComponent.ts +++ b/src/classes/IOComponent.ts @@ -1,4 +1,4 @@ -import { z } from 'zod' +import { z, ZodError } from 'zod' import { ioSchema, resolvesImmediately, @@ -7,6 +7,7 @@ import { T_IO_RETURNS, } from '../ioSchema' import { deserializeDates } from '../utils/deserialize' +import IOError from './IOError' import { IOPromiseValidator } from './IOPromise' type IoSchema = typeof ioSchema @@ -155,7 +156,12 @@ export default class IOComponent { this.resolver(parsed) } } catch (err) { - console.error('Received invalid return value:', err) + const ioError = new IOError( + 'BAD_RESPONSE', + 'Received invalid return value', + { cause: err } + ) + throw ioError } } @@ -178,7 +184,18 @@ export default class IOComponent { } this.onStateChangeHandler && this.onStateChangeHandler() } catch (err) { - console.error('Received invalid state:', err) + if (err instanceof ZodError) { + const ioError = new IOError('BAD_RESPONSE', 'Received invalid state') + ioError.cause = err + throw ioError + } else { + const ioError = new IOError( + 'RESPONSE_HANDLER_ERROR', + 'Error in state change handler' + ) + ioError.cause = err + throw ioError + } } return this.instance diff --git a/src/classes/IOError.ts b/src/classes/IOError.ts index bcad56d..efd671d 100644 --- a/src/classes/IOError.ts +++ b/src/classes/IOError.ts @@ -1,10 +1,15 @@ -export type IOErrorKind = 'CANCELED' | 'TRANSACTION_CLOSED' | 'BAD_RESPONSE' +export type IOErrorKind = + | 'CANCELED' + | 'TRANSACTION_CLOSED' + | 'BAD_RESPONSE' + | 'RESPONSE_HANDLER_ERROR' export default class IOError extends Error { kind: IOErrorKind - constructor(kind: IOErrorKind, message?: string) { - super(message) + constructor(kind: IOErrorKind, message?: string, options?: { cause?: any }) { + super(message, options) this.kind = kind + this.name = 'IOError' } } diff --git a/src/classes/IntervalClient.ts b/src/classes/IntervalClient.ts index 0c04332..18bbeeb 100644 --- a/src/classes/IntervalClient.ts +++ b/src/classes/IntervalClient.ts @@ -59,8 +59,6 @@ import { Layout, BasicLayout, LayoutSchemaInput, - MetaItemSchema, - MetaItemsSchema, BasicLayoutConfig, } from './Layout' @@ -473,7 +471,7 @@ export default class IntervalClient { }) .catch(async err => { if (err instanceof IOError) { - this.#logger.error( + this.#logger.warn( 'Failed resending pending IO call: ', err.kind ) @@ -534,7 +532,7 @@ export default class IntervalClient { }) .catch(async err => { if (err instanceof IOError) { - this.#logger.error( + this.#logger.warn( 'Failed resending transaction loading state: ', err.kind ) @@ -655,14 +653,14 @@ export default class IntervalClient { } catch (err) { this.#logger.warn('Pong not received in time') if (!(err instanceof TimeoutError)) { - this.#logger.error(err) + this.#logger.warn(err) } if ( lastSuccessfulPing.getTime() < new Date().getTime() - this.#closeUnresponsiveConnectionTimeoutMs ) { - this.#logger.error( + this.#logger.warn( 'No pong received in last three minutes, closing connection to Interval and retrying...' ) if (this.#pingIntervalHandle) { @@ -769,7 +767,7 @@ export default class IntervalClient { inputs.type as DescriptionType ) } else { - this.#logger.warn( + this.#logger.debug( 'INITIALIZE_PEER_CONNECTION:', 'DCC not found for inputs', inputs @@ -796,7 +794,7 @@ export default class IntervalClient { } } } catch (err) { - this.#logger.error('Failed initializing peer connection', err) + this.#logger.warn('Failed initializing peer connection', err) } }, START_TRANSACTION: async inputs => { @@ -960,12 +958,28 @@ export default class IntervalClient { intervalClient.#logger.error(err) + let data: IOFunctionReturnType = null + if (err instanceof IOError && err.cause) { + err = err.cause + } + + if (err instanceof Error) { + data = { + error: err.name, + message: err.message, + cause: + err.cause && err.cause instanceof Error + ? `${err.cause.name}: ${err.cause.message}` + : undefined, + // TODO: Maybe show stack traces in the future? + // stack: err.stack, + } + } + const result: ActionResultSchema = { schemaVersion: TRANSACTION_RESULT_SCHEMA_VERSION, status: 'FAILURE', - data: err.message - ? { error: err.name, message: err.message } - : null, + data, } return result @@ -1199,7 +1213,7 @@ export default class IntervalClient { pageSendTimeout = null sendPagePromise = sendPage() .catch(err => { - this.#logger.error(`Failed sending page with key ${pageKey}`, err) + this.#logger.debug(`Failed sending page with key ${pageKey}`, err) }) .finally(() => { sendPagePromise = null @@ -1249,6 +1263,12 @@ export default class IntervalClient { layoutKey, error: error.name, message: error.message, + cause: + error.cause && error.cause instanceof Error + ? `${error.cause.name}: ${error.cause.message}` + : undefined, + // TODO: Maybe show stack traces in the future? + // stack: error.stack, } } else { return { @@ -1334,19 +1354,34 @@ export default class IntervalClient { } if (page.children) { - group(page.children).then(() => { - this.#logger.debug( - 'Initial children render complete for pageKey', - pageKey - ) - }) + group(page.children).then( + () => { + this.#logger.debug( + 'Initial children render complete for pageKey', + pageKey + ) + }, + // We use the reject callback form because it's an IOGroupPromise, + // not a real Promise and we don't currently implement `.catch()` + // (I don't know how or if it's possbile right now, thenable objects aren't documented well) + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables + err => { + this.#logger.error(err) + if (err instanceof IOError && err.cause) { + errors.push(pageError(err.cause)) + } else { + errors.push(pageError(err)) + } + + scheduleSendPage() + } + ) } else { scheduleSendPage() } }) .catch(async err => { - this.#logger.error(err) - errors.push(pageError(err)) + this.#logger.error('Error in page:', err) const pageLayout: LayoutSchemaInput = { kind: 'BASIC', errors, diff --git a/src/classes/IntervalError.ts b/src/classes/IntervalError.ts index a3ce7cc..f46af4d 100644 --- a/src/classes/IntervalError.ts +++ b/src/classes/IntervalError.ts @@ -1,5 +1,6 @@ export default class IntervalError extends Error { constructor(message: string) { super(message) + this.name = 'IntervalError' } } diff --git a/src/classes/Layout.ts b/src/classes/Layout.ts index b45abe5..3c81218 100644 --- a/src/classes/Layout.ts +++ b/src/classes/Layout.ts @@ -62,6 +62,14 @@ export const META_ITEMS_SCHEMA = z.object({ export type MetaItemsSchema = z.infer +export const LAYOUT_ERROR_SCHEMA = z.object({ + layoutKey: z.string().optional(), + error: z.string(), + message: z.string(), + cause: z.string().optional(), + stack: z.string().optional(), +}) + export const BASIC_LAYOUT_SCHEMA = z.object({ kind: z.literal('BASIC'), title: z.string().nullish(), @@ -69,15 +77,7 @@ export const BASIC_LAYOUT_SCHEMA = z.object({ children: IO_RENDER.optional(), metadata: META_ITEMS_SCHEMA.optional(), menuItems: z.array(buttonItem).optional(), - errors: z - .array( - z.object({ - layoutKey: z.string().optional(), - error: z.string(), - message: z.string(), - }) - ) - .optional(), + errors: z.array(LAYOUT_ERROR_SCHEMA).optional(), }) // To be extended with z.discriminatedUnion when adding different pages @@ -87,5 +87,6 @@ export type LayoutSchema = z.infer export type LayoutSchemaInput = z.input export type BasicLayoutSchema = z.infer export type BasicLayoutSchemaInput = z.input +export type LayoutError = z.infer export { metaItemSchema as META_ITEM_SCHEMA } diff --git a/src/classes/Logger.ts b/src/classes/Logger.ts index b2daf88..16bf245 100644 --- a/src/classes/Logger.ts +++ b/src/classes/Logger.ts @@ -5,12 +5,16 @@ import { } from '../utils/packageManager' import * as pkg from '../../package.json' -export type LogLevel = 'prod' | 'debug' +export type LogLevel = + | 'quiet' + | 'info' + | 'prod' /* @deprecated, alias for 'info' */ + | 'debug' export const CHANGELOG_URL = 'https://interval.com/changelog' export default class Logger { - logLevel: LogLevel = 'prod' + logLevel: LogLevel = 'info' constructor(logLevel?: LogLevel) { if (logLevel) { @@ -18,18 +22,41 @@ export default class Logger { } } + /* Important messages, always emitted */ prod(...args: any[]) { console.log('[Interval] ', ...args) } - warn(...args: any[]) { - console.warn('[Interval] ', ...args) + /* Same as prod, but without the [Interval] prefix */ + prodNoPrefix(...args: any[]) { + console.log(...args) } + /* Fatal errors or errors in user code, always emitted */ error(...args: any[]) { console.error('[Interval] ', ...args) } + /* Informational messages, not emitted in "quiet" logLevel */ + info(...args: any[]) { + if (this.logLevel !== 'quiet') { + console.info('[Interval] ', ...args) + } + } + + /* Same as info, but without the [Interval] prefix */ + infoNoPrefix(...args: any[]) { + console.log(...args) + } + + /* Non-fatal warnings, not emitted in "quiet" logLevel */ + warn(...args: any[]) { + if (this.logLevel !== 'quiet') { + console.warn('[Interval] ', ...args) + } + } + + /* Debugging/tracing information, only emitted in "debug" logLevel */ debug(...args: any[]) { if (this.logLevel === 'debug') { console.debug('[Interval] ', ...args) @@ -37,7 +64,7 @@ export default class Logger { } handleSdkAlert(sdkAlert: SdkAlert) { - console.log('') + console.info('') const WARN_EMOJI = '\u26A0\uFE0F' const ERROR_EMOJI = '‼️' @@ -46,30 +73,39 @@ export default class Logger { switch (severity) { case 'INFO': - this.prod('🆕\tA new Interval SDK version is available.') + this.info('🆕\tA new Interval SDK version is available.') + if (message) { + this.info(message) + } break case 'WARNING': - this.prod( + this.warn( `${WARN_EMOJI}\tThis version of the Interval SDK has been deprecated. Please update as soon as possible, it will not work in a future update.` ) + if (message) { + this.warn(message) + } break case 'ERROR': - this.prod( + this.error( `${ERROR_EMOJI}\tThis version of the Interval SDK is no longer supported. Your app will not work until you update.` ) + if (message) { + this.error(message) + } break + default: + if (message) { + this.prod(message) + } } - if (message) { - this.prod(message) - } - - this.prod("\t- See what's new at:", CHANGELOG_URL) - this.prod( + this.info("\t- See what's new at:", CHANGELOG_URL) + this.info( '\t- Update now by running:', getInstallCommand(`${pkg.name}@latest`, detectPackageManager()) ) - console.log('') + console.info('') } } diff --git a/src/classes/TransactionLoadingState.ts b/src/classes/TransactionLoadingState.ts index d6068e5..6d068e9 100644 --- a/src/classes/TransactionLoadingState.ts +++ b/src/classes/TransactionLoadingState.ts @@ -75,7 +75,7 @@ export default class TransactionLoadingState { async completeOne() { if (!this.#state || !this.#state.itemsInQueue) { this.#logger.warn( - 'Please call `loading.start` with `itemsInQueue` before `loading.completeOne`, failing to do so does nothing.' + 'Please call `loading.start` with `itemsInQueue` before `loading.completeOne`, nothing to complete.' ) return } diff --git a/src/components/displayTable.ts b/src/components/displayTable.ts index bf218a4..7529228 100644 --- a/src/components/displayTable.ts +++ b/src/components/displayTable.ts @@ -17,7 +17,7 @@ type PublicProps> = Omit< T_IO_PROPS<'DISPLAY_TABLE'>, 'data' | 'columns' | 'totalRecords' | 'isAsync' > & { - columns?: (TableColumn | string)[] + columns?: (TableColumn | (string & keyof Row))[] rowMenuItems?: (row: Row) => MenuItem[] } & ( | { @@ -33,7 +33,7 @@ export default function displayTable(logger: Logger) { props: PublicProps ) { const initialColumns = columnsBuilder(props, column => - logger.error(missingColumnMessage('io.display.table')(column)) + logger.warn(missingColumnMessage('io.display.table')(column)) ) // Rendering all rows on initialization is necessary for filtering and sorting @@ -74,7 +74,7 @@ export default function displayTable(logger: Logger) { data, }, column => - logger.error(missingColumnMessage('io.display.table')(column)) + logger.warn(missingColumnMessage('io.display.table')(column)) ) serializedData = data.map((row, index) => tableRowSerializer({ diff --git a/src/components/selectTable.ts b/src/components/selectTable.ts index 410e2eb..c24b657 100644 --- a/src/components/selectTable.ts +++ b/src/components/selectTable.ts @@ -13,7 +13,7 @@ import { type PublicProps = Omit, 'data' | 'columns'> & { data: Row[] - columns?: (TableColumn | string)[] + columns?: (TableColumn | (string & keyof Row))[] rowMenuItems?: (row: Row) => MenuItem[] } @@ -24,7 +24,7 @@ export default function selectTable(logger: Logger) { type DataList = typeof props['data'] const columns = columnsBuilder(props, column => - logger.error(missingColumnMessage('io.select.table')(column)) + logger.warn(missingColumnMessage('io.select.table')(column)) ) // Rendering all rows on initialization is necessary for filtering and sorting diff --git a/src/components/upload.ts b/src/components/upload.ts index b7b5017..0e3e337 100644 --- a/src/components/upload.ts +++ b/src/components/upload.ts @@ -69,6 +69,7 @@ export function file(logger: Logger) { const urls = await generatePresignedUrls(newState) return urls } catch (error) { + // TODO: We should not swallow this error after merging #1012 logger.error( 'An error was unexpectedly thrown from the `generatePresignedUrls` function:' ) diff --git a/src/examples/basic/index.ts b/src/examples/basic/index.ts index d3d9948..a9f4457 100644 --- a/src/examples/basic/index.ts +++ b/src/examples/basic/index.ts @@ -1,5 +1,5 @@ import { T_IO_PROPS } from './../../ioSchema' -import Interval, { IOError, io, ctx, Page, Layout } from '../../index' +import Interval, { IOError, io, ctx, Page } from '../../index' import IntervalClient from '../../classes/IntervalClient' import { IntervalActionDefinition, @@ -14,7 +14,6 @@ import unauthorized from './unauthorized' import './ghostHost' import { generateS3Urls } from '../utils/upload' import fs from 'fs' -import fakeUsers from '../utils/fakeUsers' const actionLinks: IntervalActionHandler = async () => { await io.group([ @@ -190,59 +189,6 @@ const interval = new Interval({ logLevel: 'debug', endpoint: 'ws://localhost:3000/websocket', routes: { - big_table: new Page({ - name: 'Big table', - handler: async () => { - const bigData = [ - ...fakeUsers, - ...fakeUsers, - ...fakeUsers, - ...fakeUsers, - ...fakeUsers, - ...fakeUsers, - ...fakeUsers, - ...fakeUsers, - ...fakeUsers, - ...fakeUsers, - ] - - return new Layout({ - children: [ - io.display.table('Large table', { - data: bigData, - // These don't work, they're just here to make the payload bigger - rowMenuItems: row => [ - { - label: 'Browse app structure', - action: 'organizations/app_structure', - params: { org: row.email }, - }, - { - label: 'Change slug', - action: 'organizations/change_slug', - params: { org: row.email }, - }, - { - label: 'Enable SSO', - action: 'organizations/create_org_sso', - params: { org: row.email }, - }, - { - label: 'Toggle feature flag', - action: 'organizations/org_feature_flag', - params: { org: row.email }, - }, - { - label: 'Transfer owner', - action: 'organizations/transfer_ownership', - params: { org: row.email }, - }, - ], - }), - ], - }) - }, - }), two_searches: async io => { const [r1, r2] = await io.group([ io.search('One', { diff --git a/src/examples/basic/table.ts b/src/examples/basic/table.ts index 355a3a5..628c17b 100644 --- a/src/examples/basic/table.ts +++ b/src/examples/basic/table.ts @@ -1,5 +1,5 @@ import { IntervalActionDefinition } from '@interval/sdk/src/types' -import { IntervalActionHandler } from '../..' +import { IntervalActionHandler, Page, Layout, io } from '../..' import { faker } from '@faker-js/faker' function generateRows(count: number, offset = 0) { @@ -153,9 +153,52 @@ export const multiple_tables: IntervalActionHandler = async io => { ]) } -export const async_table: IntervalActionHandler = async io => { - const allData = generateRows(500) - await io.display.table[0]>('Display users', { +export const big_payload_table = new Page({ + name: 'Big table', + handler: async () => { + const bigData = generateRows(100000) + + return new Layout({ + children: [ + io.display.table('Large table', { + data: bigData, + // These don't work, they're just here to make the payload bigger + rowMenuItems: row => [ + { + label: 'Browse app structure', + action: 'organizations/app_structure', + params: { org: row.email }, + }, + { + label: 'Change slug', + action: 'organizations/change_slug', + params: { org: row.email }, + }, + { + label: 'Enable SSO', + action: 'organizations/create_org_sso', + params: { org: row.email }, + }, + { + label: 'Toggle feature flag', + action: 'organizations/org_feature_flag', + params: { org: row.email }, + }, + { + label: 'Transfer owner', + action: 'organizations/transfer_ownership', + params: { org: row.email }, + }, + ], + }), + ], + }) + }, +}) + +function asyncTable(numRows: number) { + const allData = generateRows(numRows) + return io.display.table[0]>('Display users', { async getData({ queryTerm, sortColumn, sortDirection, offset, pageSize }) { let filteredData = allData.slice() @@ -198,14 +241,18 @@ export const async_table: IntervalActionHandler = async io => { 'id', { label: 'User', - renderCell: row => ({ - label: row.name, - image: { - alt: 'Alt tag', - url: row.image, - size: 'small', - }, - }), + renderCell: row => { + throw new Error('Oops!') + + return { + label: row.name, + image: { + alt: 'Alt tag', + url: row.image, + size: 'small', + }, + } + }, }, { label: 'Email', @@ -234,6 +281,19 @@ export const async_table: IntervalActionHandler = async io => { }) } +export const async_table_page = new Page({ + name: 'Async table - in a page', + handler: async () => { + return new Layout({ + children: [asyncTable(500)], + }) + }, +}) + +export const async_table: IntervalActionHandler = async () => { + await asyncTable(500) +} + export const select_table: IntervalActionHandler = async io => { faker.seed(0) diff --git a/src/index.ts b/src/index.ts index 7164b0a..ed3b22c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { z } from 'zod' import fetch from 'cross-fetch' import Routes from './classes/Routes' import IOError from './classes/IOError' -import Logger from './classes/Logger' +import Logger, { LogLevel } from './classes/Logger' import Page from './classes/Page' import { NOTIFY, @@ -54,7 +54,7 @@ export interface InternalConfig { // TODO: Mark as deprecated soon, remove soon afterward groups?: Record endpoint?: string - logLevel?: 'prod' | 'debug' + logLevel?: LogLevel retryIntervalMs?: number retryChunkIntervalMs?: number pingIntervalMs?: number diff --git a/src/types.ts b/src/types.ts index 1a02f27..2ba174a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -195,11 +195,13 @@ export type ComponentsRenderer< components: Components, validator?: IOClientRenderValidator, continueButton?: ButtonConfig -) => Promise<{ - [Idx in keyof Components]: Components[Idx] extends AnyIOComponent - ? z.infer | undefined - : Components[Idx] -}> +) => Promise< + { + [Idx in keyof Components]: Components[Idx] extends AnyIOComponent + ? z.infer | undefined + : Components[Idx] + } +> export type IORenderSender = (ioToRender: T_IO_RENDER_INPUT) => Promise @@ -363,15 +365,17 @@ export type TableColumnResult = } | TableCellValue +export type ColumnKey = string & keyof Row + export type TableColumn = { label: string } & ( | { - accessorKey: string + accessorKey: string & keyof Row renderCell?: (row: Row) => TableColumnResult } | { - accessorKey?: string + accessorKey?: string & keyof Row renderCell: (row: Row) => TableColumnResult } ) @@ -379,5 +383,6 @@ export type TableColumn = { export type PageError = { error: string message: string + cause?: string layoutKey?: keyof BasicLayoutConfig } diff --git a/src/utils/fileActionLoader.ts b/src/utils/fileActionLoader.ts index 3c05fee..4151ce5 100644 --- a/src/utils/fileActionLoader.ts +++ b/src/utils/fileActionLoader.ts @@ -59,7 +59,7 @@ async function loadFolder(currentDirectory: string, logger: Logger) { } } } catch (err) { - logger.error( + logger.warn( `Failed loading file at ${fullPath} as module, skipping.`, err ) diff --git a/src/utils/table.ts b/src/utils/table.ts index 848239d..d6e75a6 100644 --- a/src/utils/table.ts +++ b/src/utils/table.ts @@ -1,5 +1,5 @@ import { internalTableColumn, tableRow, internalTableRow } from '../ioSchema' -import { MenuItem } from '../types' +import { ColumnKey, MenuItem } from '../types' import { z } from 'zod' import { TableColumn, TableColumnResult } from '../types' import { bufferToDataUrl } from './image' @@ -15,7 +15,7 @@ export const TABLE_DATA_BUFFER_SIZE = 500 */ export function columnsBuilder>( props: { - columns?: (TableColumn | string)[] + columns?: (TableColumn | ColumnKey)[] data?: Row[] }, logMissingColumn: (column: string) => void @@ -29,12 +29,13 @@ export function columnsBuilder>( return labels.map(label => ({ label, - accessorKey: label, + accessorKey: label as ColumnKey, })) } return props.columns.map(column => { const accessorKey = typeof column === 'string' ? column : column.accessorKey + if (accessorKey && !dataColumns.has(accessorKey)) { logMissingColumn(accessorKey) }