diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 3d11a081..c1de1780 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -142,14 +142,26 @@ export const selectedCellAtom = atom(null); export const contextMenuTargetAtom = atom(null); export type CloudLogFilters = { - type: "webhook" | "functions" | "audit" | "build"; + type: "extension" | "webhook" | "column" | "audit" | "build" | "functions"; timeRange: | { type: "seconds" | "minutes" | "hours" | "days"; value: number } | { type: "range"; start: Date; end: Date }; severity?: Array; webhook?: string[]; + extension?: string[]; + column?: string[]; auditRowId?: string; buildLogExpanded?: number; + functionType?: ( + | "connector" + | "derivative-script" + | "action" + | "derivative-function" + | "extension" + | "defaultValue" + | "hooks" + )[]; + loggingSource?: ("backend-scripts" | "backend-function" | "hooks")[]; }; /** Store cloud log modal filters in URL */ export const cloudLogFiltersAtom = atomWithHash( diff --git a/src/components/CodeEditor/CodeEditorHelper.tsx b/src/components/CodeEditor/CodeEditorHelper.tsx index 299c2134..04326e3c 100644 --- a/src/components/CodeEditor/CodeEditorHelper.tsx +++ b/src/components/CodeEditor/CodeEditorHelper.tsx @@ -38,6 +38,10 @@ export default function CodeEditorHelper({ key: "rowy", description: `rowy provides a set of functions that are commonly used, such as easy file uploads & access to GCP Secret Manager`, }, + { + key: "logging", + description: `logging.log is encouraged to replace console.log`, + }, ]; return ( diff --git a/src/components/CodeEditor/extensions.d.ts b/src/components/CodeEditor/extensions.d.ts index dd592d05..22af5756 100644 --- a/src/components/CodeEditor/extensions.d.ts +++ b/src/components/CodeEditor/extensions.d.ts @@ -26,6 +26,7 @@ type ExtensionContext = { extensionBody: any; }; RULES_UTILS: any; + logging: RowyLogging; }; // extension body definition diff --git a/src/components/CodeEditor/rowy.d.ts b/src/components/CodeEditor/rowy.d.ts index f6852cce..42582dd0 100644 --- a/src/components/CodeEditor/rowy.d.ts +++ b/src/components/CodeEditor/rowy.d.ts @@ -17,6 +17,11 @@ type uploadOptions = { folderPath?: string; fileName?: string; }; +type RowyLogging = { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; +}; interface Rowy { metadata: { /** diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index cf025217..7dff465e 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -20,11 +20,11 @@ import { ColumnPlusBefore as ColumnPlusBeforeIcon, ColumnPlusAfter as ColumnPlusAfterIcon, ColumnRemove as ColumnRemoveIcon, + CloudLogs as LogsIcon, } from "@src/assets/icons"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; import EditIcon from "@mui/icons-material/EditOutlined"; -// import ReorderIcon from "@mui/icons-material/Reorder"; import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import EvalIcon from "@mui/icons-material/PlayCircleOutline"; @@ -51,6 +51,8 @@ import { tableFiltersPopoverAtom, tableNextPageAtom, tableSchemaAtom, + cloudLogFiltersAtom, + tableModalAtom, } from "@src/atoms/tableScope"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; @@ -107,6 +109,8 @@ export default function ColumnMenu({ ); const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const setModal = useSetAtom(tableModalAtom, tableScope); + const setCloudLogFilters = useSetAtom(cloudLogFiltersAtom, tableScope); const snackLogContext = useSnackLogContext(); const [altPress] = useAtom(altPressAtom, projectScope); @@ -314,26 +318,29 @@ export default function ColumnMenu({ }, disabled: !isConfigurable, }, - // { - // label: "Re-order", - // icon: , - // onClick: () => alert("REORDER"), - // }, - - // { - // label: "Hide for everyone", - // activeLabel: "Show", - // icon: , - // activeIcon: , - // onClick: () => { - // actions.update(column.key, { hidden: !column.hidden }); - // handleClose(); - // }, - // active: column.hidden, - // color: "error" as "error", - // }, ]; + if ( + column?.config?.defaultValue?.type === "dynamic" || + [FieldType.action, FieldType.derivative, FieldType.connector].includes( + column.type + ) + ) { + configActions.push({ + key: "logs", + label: altPress ? "Logs" : "Logs…", + icon: , + onClick: () => { + setModal("cloudLogs"); + setCloudLogFilters({ + type: "column", + timeRange: { type: "days", value: 7 }, + column: [column.key], + }); + }, + }); + } + // TODO: Generalize const handleEvaluateAll = async () => { try { diff --git a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx index 5213eb8a..3cc78e2a 100644 --- a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx +++ b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx @@ -52,11 +52,11 @@ function CodeEditor({ type, column, handleChange }: ICodeEditorProps) { } else if (column.config?.defaultValue?.dynamicValueFn) { dynamicValueFn = column.config?.defaultValue?.dynamicValueFn; } else if (column.config?.defaultValue?.script) { - dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{ + dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ ${column.config?.defaultValue.script} }`; } else { - dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{ + dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ // Write your default value code here // for example: // generate random hex color diff --git a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts index 0e50bb2d..779bd9be 100644 --- a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts +++ b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts @@ -4,5 +4,6 @@ type DefaultValueContext = { storage: firebasestorage.Storage; db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; + logging: RowyLogging; }; type DefaultValue = (context: DefaultValueContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx b/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx index f82a75f1..9e0887f3 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx @@ -187,22 +187,32 @@ export default function CloudLogItem({ )} - {data.payload === "textPayload" && data.textPayload} - {get(data, "httpRequest.requestUrl")?.split(".run.app").pop()} - {data.payload === "jsonPayload" && ( - - {data.jsonPayload.error}{" "} - + {data.logName.endsWith("rowy-logging") && data.jsonPayload.payload ? ( + <> + {typeof data.jsonPayload.payload === "string" + ? data.jsonPayload.payload + : JSON.stringify(data.jsonPayload.payload)} + + ) : ( + <> + {data.payload === "textPayload" && data.textPayload} + {get(data, "httpRequest.requestUrl")?.split(".run.app").pop()} + {data.payload === "jsonPayload" && ( + + {data.jsonPayload.error}{" "} + + )} + {data.payload === "jsonPayload" && + stringify(data.jsonPayload.body ?? data.jsonPayload, { + space: 2, + })} + )} - {data.payload === "jsonPayload" && - stringify(data.jsonPayload.body ?? data.jsonPayload, { - space: 2, - })} diff --git a/src/components/TableModals/CloudLogsModal/CloudLogList.tsx b/src/components/TableModals/CloudLogsModal/CloudLogList.tsx index 5e5382ee..93093d38 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogList.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogList.tsx @@ -70,6 +70,13 @@ export default function CloudLogList({ items, ...props }: ICloudLogListProps) { "jsonPayload.rowyUser.displayName", // Webhook event "jsonPayload.params.endpoint", + // Rowy Logging + "jsonPayload.functionType", + "jsonPayload.loggingSource", + "jsonPayload.extensionName", + "jsonPayload.extensionType", + "jsonPayload.webhookName", + "jsonPayload.fieldName", ]} /> diff --git a/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx b/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx index 0cf0dc6e..f8413f65 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx @@ -22,6 +22,12 @@ export const SEVERITY_LEVELS = { EMERGENCY: "One or more systems are unusable.", }; +export const SEVERITY_LEVELS_ROWY = { + DEFAULT: "The log entry has no assigned severity level.", + WARNING: "Warning events might cause problems.", + ERROR: "Error events are likely to cause problems.", +}; + export interface ICloudLogSeverityIconProps extends SvgIconProps { severity: keyof typeof SEVERITY_LEVELS; } diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index e69acc44..76cfe719 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -1,6 +1,6 @@ import useSWR from "swr"; import { useAtom } from "jotai"; -import { startCase } from "lodash-es"; +import { startCase, upperCase } from "lodash-es"; import { ITableModalProps } from "@src/components/TableModals"; import { @@ -12,9 +12,12 @@ import { TextField, InputAdornment, Button, + Box, + CircularProgress, } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import { CloudLogs as LogsIcon } from "@src/assets/icons"; +import ClearIcon from "@mui/icons-material/Clear"; import Modal from "@src/components/Modal"; import TableToolbarButton from "@src/components/TableToolbar/TableToolbarButton"; @@ -23,7 +26,10 @@ import TimeRangeSelect from "./TimeRangeSelect"; import CloudLogList from "./CloudLogList"; import BuildLogs from "./BuildLogs"; import EmptyState from "@src/components/EmptyState"; -import CloudLogSeverityIcon, { SEVERITY_LEVELS } from "./CloudLogSeverityIcon"; +import CloudLogSeverityIcon, { + SEVERITY_LEVELS, + SEVERITY_LEVELS_ROWY, +} from "./CloudLogSeverityIcon"; import { projectScope, @@ -38,6 +44,7 @@ import { cloudLogFiltersAtom, } from "@src/atoms/tableScope"; import { cloudLogFetcher } from "./utils"; +import { FieldType } from "@src/constants/fields"; export default function CloudLogsModal({ onClose }: ITableModalProps) { const [projectId] = useAtom(projectIdAtom, projectScope); @@ -92,7 +99,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { "&, & .MuiTab-root": { minHeight: { md: "var(--dialog-title-height)" }, }, - ml: { md: 18 }, + ml: { md: 20 }, mr: { md: 40 / 8 + 3 }, minHeight: 32, @@ -110,18 +117,35 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { + onChange={(_, newType) => { setCloudLogFilters((c) => ({ - type: v, + type: newType, timeRange: c.timeRange, - })) - } + })); + if ( + [ + "extension", + "webhook", + "column", + "audit", + "functions", + ].includes(newType) + ) { + setTimeout(() => { + mutate(); + }, 0); + } + }} aria-label="Filter by log type" > - Webhooks - Functions + Extension + Webhook + Column Audit Build + + Functions (legacy) + ) : ( )} - {cloudLogFilters.type === "webhook" && ( - ({ - label: x.name, - value: x.endpoint, - })) - : [] - } - value={cloudLogFilters.webhook ?? []} - onChange={(v) => - setCloudLogFilters((prev) => ({ ...prev, webhook: v })) - } - TextFieldProps={{ - id: "webhook", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 180 } }, - fullWidth: false, - }} - itemRenderer={(option) => ( - <> - {option.label} {option.value} - - )} - /> - )} - {cloudLogFilters.type === "audit" && ( - - setCloudLogFilters((prev) => ({ - ...prev, - auditRowId: e.target.value, - })) - } - InputProps={{ - startAdornment: ( - - {tableSettings.collection}/ - - ), - }} - className="labelHorizontal" - sx={{ - "& .MuiInputBase-root, & .MuiInputBase-input": { - typography: "body2", - fontFamily: "mono", - }, - "& .MuiInputAdornment-positionStart": { - m: "0 !important", - pointerEvents: "none", - }, - "& .MuiInputBase-input": { pl: 0 }, - }} - /> - )} - - {/* Spacer */}
{cloudLogFilters.type !== "build" && ( <> - {!isValidating && Array.isArray(data) && ( - - {data.length} entries - - )} - - - setCloudLogFilters((prev) => ({ ...prev, severity })) - } - TextFieldProps={{ - style: { width: 130 }, - placeholder: "Severity", - SelectProps: { - renderValue: () => { - if ( - !Array.isArray(cloudLogFilters.severity) || - cloudLogFilters.severity.length === 0 - ) - return `Severity`; - - if (cloudLogFilters.severity.length === 1) - return ( - <> - Severity{" "} - - - ); - - return `Severity (${cloudLogFilters.severity.length})`; - }, - }, + + {isValidating ? "" : `${data?.length ?? 0} entries`} + + { + setCloudLogFilters((prev) => ({ + ...prev, + functionType: undefined, + loggingSource: undefined, + webhook: undefined, + extension: undefined, + severity: undefined, + })); }} - itemRenderer={(option) => ( - <> - - {startCase(option.value.toLowerCase())} - - )} - /> - - setCloudLogFilters((c) => ({ ...c, timeRange: value })) - } + title="Clear Filters" + icon={} + disabled={isValidating} /> mutate()} title="Refresh" - icon={} + icon={ + isValidating ? ( + + ) : ( + + ) + } disabled={isValidating} /> )} - - {isValidating && ( - - )} - - {/* {logQueryUrl} */} } > {cloudLogFilters.type === "build" ? ( - ) : Array.isArray(data) && data.length > 0 ? ( - <> - - {cloudLogFilters.timeRange.type !== "range" && ( - - )} - - ) : isValidating ? ( - ) : ( - + + {["extension", "webhook", "column", "audit", "functions"].includes( + cloudLogFilters.type + ) ? ( + + {cloudLogFilters.type === "functions" ? ( + + ) : null} + {cloudLogFilters.type === "extension" ? ( + <> + ({ + label: x.name, + value: x.name, + type: x.type, + })) + : [] + } + value={cloudLogFilters.extension ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, extension: v })) + } + TextFieldProps={{ + id: "extension", + className: "labelHorizontal", + sx: { + width: "100%", + "& .MuiInputBase-root": { width: "100%" }, + }, + fullWidth: false, + placeholder: "Extension", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.extension?.length === 1) { + return `Extension (${cloudLogFilters.extension[0]})`; + } else if (cloudLogFilters?.extension?.length) { + return `Extension (${cloudLogFilters.extension.length})`; + } else { + return `Extension`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.type} + + )} + /> + + ) : null} + {cloudLogFilters.type === "webhook" ? ( + ({ + label: x.name, + value: x.endpoint, + })) + : [] + } + value={cloudLogFilters.webhook ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, webhook: v })) + } + TextFieldProps={{ + id: "webhook", + className: "labelHorizontal", + sx: { + width: "100%", + "& .MuiInputBase-root": { width: "100%" }, + }, + fullWidth: false, + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.webhook?.length) { + return `Webhook (${cloudLogFilters.webhook.length})`; + } else { + return `Webhook`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.value} + + )} + /> + ) : null} + {cloudLogFilters.type === "column" ? ( + <> + + config?.config?.defaultValue?.type === "dynamic" || + [ + FieldType.action, + FieldType.derivative, + FieldType.connector, + ].includes(config.type) + ) + .map(([key, config]) => ({ + label: config.name, + value: key, + type: config.type, + }))} + value={cloudLogFilters.column ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, column: v })) + } + TextFieldProps={{ + id: "column", + className: "labelHorizontal", + sx: { + width: "100%", + "& .MuiInputBase-root": { width: "100%" }, + }, + fullWidth: false, + placeholder: "Column", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.column?.length === 1) { + return `Column (${cloudLogFilters.column[0]})`; + } else if (cloudLogFilters?.column?.length) { + return `Column (${cloudLogFilters.column.length})`; + } else { + return `Column`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.value}  + {option.type} + + )} + /> + + ) : null} + {cloudLogFilters.type === "audit" ? ( + <> + + setCloudLogFilters((prev) => ({ + ...prev, + auditRowId: e.target.value, + })) + } + InputProps={{ + startAdornment: ( + + {tableSettings.collection}/ + + ), + }} + className="labelHorizontal" + sx={{ + width: "100%", + "& .MuiInputBase-root, & .MuiInputBase-input": { + width: "100%", + typography: "body2", + fontFamily: "mono", + }, + "& .MuiInputAdornment-positionStart": { + m: "0 !important", + pointerEvents: "none", + }, + "& .MuiInputBase-input": { pl: 0 }, + "& .MuiFormLabel-root": { + whiteSpace: "nowrap", + }, + }} + /> + + ) : null} + + setCloudLogFilters((prev) => ({ ...prev, severity })) + } + TextFieldProps={{ + style: { width: 200 }, + placeholder: "Severity", + SelectProps: { + renderValue: () => { + if ( + !Array.isArray(cloudLogFilters.severity) || + cloudLogFilters.severity.length === 0 + ) + return `Severity`; + + if (cloudLogFilters.severity.length === 1) + return ( + <> + Severity{" "} + + + ); + + return `Severity (${cloudLogFilters.severity.length})`; + }, + }, + }} + itemRenderer={(option) => ( + <> + + {startCase(option.value.toLowerCase())} + + )} + /> + + setCloudLogFilters((c) => ({ ...c, timeRange: value })) + } + /> + + ) : null} + + {Array.isArray(data) && data.length > 0 ? ( + + + {cloudLogFilters.timeRange.type !== "range" && ( + + )} + + ) : isValidating ? ( + + ) : ( + + )} + + )} ); diff --git a/src/components/TableModals/CloudLogsModal/TimeRangeSelect.tsx b/src/components/TableModals/CloudLogsModal/TimeRangeSelect.tsx index b6da74a8..9450348a 100644 --- a/src/components/TableModals/CloudLogsModal/TimeRangeSelect.tsx +++ b/src/components/TableModals/CloudLogsModal/TimeRangeSelect.tsx @@ -19,7 +19,9 @@ export default function TimeRangeSelect({ ...props }: ITimeRangeSelectProps) { return ( -
+
{value && value.type !== "range" && ( { + return `jsonPayload.loggingSource = "${loggingSource}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } + switch (cloudLogFilters.type) { + case "extension": + logQuery.push(`logName = "projects/${projectId}/logs/rowy-logging"`); + if (cloudLogFilters?.extension?.length) { + logQuery.push( + cloudLogFilters.extension + .map((extensionName) => { + return `jsonPayload.extensionName = "${extensionName}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } else { + logQuery.push(`jsonPayload.functionType = "extension"`); + } + break; + case "webhook": - logQuery.push( - `logName = "projects/${projectId}/logs/rowy-webhook-events"` - ); - logQuery.push(`jsonPayload.url : "${tablePath}"`); - if ( - Array.isArray(cloudLogFilters.webhook) && - cloudLogFilters.webhook.length > 0 - ) + if (cloudLogFilters?.webhook?.length) { logQuery.push( cloudLogFilters.webhook .map((id) => `jsonPayload.url : "${id}"`) .join(encodeURIComponent(" OR ")) ); + } else { + logQuery.push(`jsonPayload.functionType = "hooks"`); + } + break; + + case "column": + if (cloudLogFilters?.column?.length) { + logQuery.push( + cloudLogFilters.column + .map((column) => { + return `jsonPayload.fieldName = "${column}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } else { + logQuery.push( + [ + "connector", + "derivative-script", + "action", + "derivative-function", + "defaultValue", + ] + .map((functionType) => { + return `jsonPayload.functionType = "${functionType}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } break; case "audit": diff --git a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx index dbd63a9e..a0d5b82f 100644 --- a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx +++ b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx @@ -14,6 +14,7 @@ import { import { Extension as ExtensionIcon, Copy as DuplicateIcon, + CloudLogs as LogsIcon, } from "@src/assets/icons"; import EditIcon from "@mui/icons-material/EditOutlined"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; @@ -21,6 +22,12 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import EmptyState from "@src/components/EmptyState"; import { extensionNames, IExtension } from "./utils"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; +import { useSetAtom } from "jotai"; +import { + cloudLogFiltersAtom, + tableModalAtom, + tableScope, +} from "@src/atoms/tableScope"; export interface IExtensionListProps { extensions: IExtension[]; @@ -37,6 +44,9 @@ export default function ExtensionList({ handleEdit, handleDelete, }: IExtensionListProps) { + const setModal = useSetAtom(tableModalAtom, tableScope); + const setCloudLogFilters = useSetAtom(cloudLogFiltersAtom, tableScope); + if (extensions.length === 0) return ( + + { + setModal("cloudLogs"); + setCloudLogFilters({ + type: "extension", + timeRange: { type: "days", value: 7 }, + extension: [extensionObject.name], + }); + }} + > + + + { + task: `const extensionBody: TaskBody = async({row, db, change, ref, logging}) => { // task extensions are very flexible you can do anything from updating other documents in your database, to making an api request to 3rd party service. // example: @@ -87,7 +87,7 @@ const extensionBodyTemplate = { }) */ }`, - docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref}) => { + docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -96,7 +96,7 @@ const extensionBodyTemplate = { targetPath: "", // fill in the path here }) }`, - historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref}) => { + historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -104,7 +104,7 @@ const extensionBodyTemplate = { collectionId: "historySnapshots", // optionally change the sub-collection id of where the history snapshots are stored }) }`, - algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref}) => { + algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -114,7 +114,7 @@ const extensionBodyTemplate = { objectID: ref.id, // algolia object ID, ref.id is one possible choice }) }`, - meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref}) => { + meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return({ @@ -124,7 +124,7 @@ const extensionBodyTemplate = { objectID: ref.id, // algolia object ID, ref.id is one possible choice }) }`, - bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref}) => { + bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -134,7 +134,7 @@ const extensionBodyTemplate = { objectID: ref.id, // algolia object ID, ref.id is one possible choice }) }`, - slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref}) => { + slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -144,7 +144,7 @@ const extensionBodyTemplate = { attachments: [], // the attachments parameter to pass in to slack api }) }`, - sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref}) => { + sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -164,7 +164,7 @@ const extensionBodyTemplate = { }, }) }`, - apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref}) => { + apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -174,7 +174,7 @@ const extensionBodyTemplate = { callback: ()=>{}, }) }`, - twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref}) => { + twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref, logging}) => { /** * * Setup twilio secret key: https://docs.rowy.io/extensions/twilio-message#secret-manager-setup @@ -190,7 +190,7 @@ const extensionBodyTemplate = { body: "Hi there!" // message text }) }`, - pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref}) => { + pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref, logging}) => { // you can FCM token from the row or from the user document in the database // const FCMtoken = row.FCMtoken // or push through topic @@ -238,13 +238,14 @@ export function emptyExtensionObject( extensionBody: extensionBodyTemplate[type] ?? extensionBodyTemplate["task"], requiredFields: [], trackedFields: [], - conditions: `const condition: Condition = async({row, change}) => { + conditions: `const condition: Condition = async({row, change, logging}) => { // feel free to add your own code logic here return true; }`, lastEditor: user, }; } + export function sparkToExtensionObjects( sparkConfig: string, user: IExtensionEditor diff --git a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx index 7f9e4d11..9d8e7b8c 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx @@ -21,17 +21,33 @@ const requestType = [ export const parserExtraLibs = [ requestType, - `type Parser = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Parser = ( + args: { + req: WebHookRequest; + db: FirebaseFirestore.Firestore; + ref: FirebaseFirestore.CollectionReference; + res: { + send: (v:any)=>void; + sendStatus: (status:number)=>void + }; + logging: RowyLogging; + } + ) => Promise;`, ]; export const conditionExtraLibs = [ requestType, - `type Condition = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Condition = ( + args: { + req: WebHookRequest; + db: FirebaseFirestore.Firestore; + ref: FirebaseFirestore.CollectionReference; + res: { + send: (v:any)=>void; + sendStatus: (status:number)=>void; + }; + logging: RowyLogging; + } + ) => Promise;`, ]; const additionalVariables = [ @@ -48,30 +64,29 @@ export const webhookBasic = { extraLibs: parserExtraLibs, template: ( table: TableSettings - ) => `const basicParser: Parser = async({req, db,ref}) => { - // request is the request object from the webhook - // db is the database object - // ref is the reference to collection of the table - // the returned object will be added as a new row to the table - // eg: adding the webhook body as row - const {body} = req; - ${ - table.audit !== false - ? ` - // auditField - const ${ - table.auditFieldCreatedBy ?? "_createdBy" - } = await rowy.metadata.serviceAccountUser() - return { - ...body, - ${table.auditFieldCreatedBy ?? "_createdBy"} - } - ` - : ` - return body; - ` - } - + ) => `const basicParser: Parser = async({req, db, ref, logging}) => { + // request is the request object from the webhook + // db is the database object + // ref is the reference to collection of the table + // the returned object will be added as a new row to the table + // eg: adding the webhook body as row + const {body} = req; + ${ + table.audit !== false + ? ` + // auditField + const ${ + table.auditFieldCreatedBy ?? "_createdBy" + } = await rowy.metadata.serviceAccountUser() + return { + ...body, + ${table.auditFieldCreatedBy ?? "_createdBy"} + } + ` + : ` + return body; + ` + } }`, }, condition: { @@ -79,7 +94,7 @@ export const webhookBasic = { extraLibs: conditionExtraLibs, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx index b557dd78..b251697f 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx @@ -13,7 +13,7 @@ export const webhookSendgrid = { extraLibs: null, template: ( table: TableSettings - ) => `const sendgridParser: Parser = async ({ req, db, ref }) => { + ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { const { body } = req const eventHandler = async (sgEvent) => { // Event handlers can be modiefed to preform different actions based on the sendgrid event @@ -35,7 +35,7 @@ export const webhookSendgrid = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx index 31eb4aff..acfb2dfe 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx @@ -17,7 +17,7 @@ export const webhookStripe = { extraLibs: null, template: ( table: TableSettings - ) => `const sendgridParser: Parser = async ({ req, db, ref }) => { + ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { const event = req.body switch (event.type) { case "payment_intent.succeeded": @@ -34,7 +34,7 @@ export const webhookStripe = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx index 5fd4ce7d..9c509efd 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx @@ -13,7 +13,7 @@ export const webhookTypeform = { extraLibs: null, template: ( table: TableSettings - ) => `const typeformParser: Parser = async({req, db,ref}) =>{ + ) => `const typeformParser: Parser = async({req, db, ref, logging}) =>{ // this reduces the form submission into a single object of key value pairs // eg: {name: "John", age: 20} // ⚠️ ensure that you have assigned ref values of the fields @@ -73,7 +73,7 @@ export const webhookTypeform = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx index bf5e8cda..7bed061d 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx @@ -14,7 +14,7 @@ export const webhook = { extraLibs: null, template: ( table: TableSettings - ) => `const formParser: Parser = async({req, db,ref}) => { + ) => `const formParser: Parser = async({req, db, ref, logging}) => { // request is the request object from the webhook // db is the database object // ref is the reference to collection of the table @@ -45,7 +45,7 @@ export const webhook = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index bb27a64a..c4292604 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -26,17 +26,33 @@ const requestType = [ export const parserExtraLibs = [ requestType, - `type Parser = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Parser = ( + args: { + req: WebHookRequest; + db: FirebaseFirestore.Firestore; + ref: FirebaseFirestore.CollectionReference; + res: { + send: (v:any)=>void; + sendStatus: (status:number)=>void + }; + logging: RowyLogging; + } + ) => Promise;`, ]; export const conditionExtraLibs = [ requestType, - `type Condition = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Condition = ( + args: { + req:WebHookRequest, + db: FirebaseFirestore.Firestore, + ref: FirebaseFirestore.CollectionReference, + res: { + send: (v:any)=>void + sendStatus: (status:number)=>void + }; + logging: RowyLogging; + } + ) => Promise;`, ]; const additionalVariables = [ diff --git a/src/components/TableModals/WebhooksModal/webhooks.d.ts b/src/components/TableModals/WebhooksModal/webhooks.d.ts index 4a8715f8..65df0530 100644 --- a/src/components/TableModals/WebhooksModal/webhooks.d.ts +++ b/src/components/TableModals/WebhooksModal/webhooks.d.ts @@ -3,10 +3,12 @@ type Condition = (args: { db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; res: Response; + logging: RowyLogging; }) => Promise; type Parser = (args: { req: WebHookRequest; db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; + logging: RowyLogging; }) => Promise; diff --git a/src/components/fields/Action/Settings.tsx b/src/components/fields/Action/Settings.tsx index 1933818c..735423ef 100644 --- a/src/components/fields/Action/Settings.tsx +++ b/src/components/fields/Action/Settings.tsx @@ -130,7 +130,7 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { : config?.runFn ? config.runFn : config?.script - ? `const action:Action = async ({row,ref,db,storage,auth,actionParams,user}) => { + ? `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} }` : RUN_ACTION_TEMPLATE; @@ -140,7 +140,7 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { : config.undoFn ? config.undoFn : get(config, "undo.script") - ? `const action : Action = async ({row,ref,db,storage,auth,actionParams,user}) => { + ? `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { ${get(config, "undo.script")} }` : UNDO_ACTION_TEMPLATE; diff --git a/src/components/fields/Action/action.d.ts b/src/components/fields/Action/action.d.ts index a4339978..1daab613 100644 --- a/src/components/fields/Action/action.d.ts +++ b/src/components/fields/Action/action.d.ts @@ -15,6 +15,7 @@ type ActionContext = { auth: firebaseauth.BaseAuth; actionParams: actionParams; user: ActionUser; + logging: RowyLogging; }; type ActionResult = { diff --git a/src/components/fields/Action/templates.ts b/src/components/fields/Action/templates.ts index 5701ef24..04b3ab23 100644 --- a/src/components/fields/Action/templates.ts +++ b/src/components/fields/Action/templates.ts @@ -1,4 +1,4 @@ -export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user}) => { +export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { // Write your action code here // for example: // const authToken = await rowy.secrets.get("service") @@ -26,7 +26,7 @@ export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,sto // checkout the documentation for more info: https://docs.rowy.io/field-types/action#script }`; -export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user}) => { +export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { // Write your undo code here // for example: // const authToken = await rowy.secrets.get("service") diff --git a/src/components/fields/Connector/connector.d.ts b/src/components/fields/Connector/connector.d.ts index d031d64b..c16585ae 100644 --- a/src/components/fields/Connector/connector.d.ts +++ b/src/components/fields/Connector/connector.d.ts @@ -15,6 +15,7 @@ type ConnectorContext = { auth: firebaseauth.BaseAuth; query: string; user: ConnectorUser; + logging: RowyLogging; }; type ConnectorResult = any[]; type Connector = ( diff --git a/src/components/fields/Connector/utils.ts b/src/components/fields/Connector/utils.ts index 17687fe7..88959895 100644 --- a/src/components/fields/Connector/utils.ts +++ b/src/components/fields/Connector/utils.ts @@ -11,7 +11,7 @@ export const replacer = (data: any) => (m: string, key: string) => { return get(data, objKey, defaultValue); }; -export const baseFunction = `const connectorFn: Connector = async ({query, row, user}) => { +export const baseFunction = `const connectorFn: Connector = async ({query, row, user, logging}) => { // TODO: Implement your service function here return []; };`; diff --git a/src/components/fields/Derivative/Settings.tsx b/src/components/fields/Derivative/Settings.tsx index 32ce65dc..7fe36869 100644 --- a/src/components/fields/Derivative/Settings.tsx +++ b/src/components/fields/Derivative/Settings.tsx @@ -65,10 +65,10 @@ export default function Settings({ : config.derivativeFn ? config.derivativeFn : config?.script - ? `const derivative:Derivative = async ({row,ref,db,storage,auth})=>{ + ? `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} }` - : `const derivative:Derivative = async ({row,ref,db,storage,auth})=>{ + : `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ // Write your derivative code here // for example: // const sum = row.a + row.b; diff --git a/src/components/fields/Derivative/derivative.d.ts b/src/components/fields/Derivative/derivative.d.ts index 3af58e11..a56afeba 100644 --- a/src/components/fields/Derivative/derivative.d.ts +++ b/src/components/fields/Derivative/derivative.d.ts @@ -5,6 +5,7 @@ type DerivativeContext = { db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; change: any; + logging: RowyLogging; }; type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE";