mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
@@ -150,5 +150,5 @@ At [Antler](https://antler.co), we identify and invest in exceptional people.
|
||||
We’re a global startup generator and early-stage VC firm that builds
|
||||
groundbreaking technology companies.
|
||||
|
||||
[Apply now](https://antler.co/apply) to be part of a global cohort of tech
|
||||
[Apply now](https://www.antler.co/apply?utm_source=Firetable&utm_medium=website&utm_campaign=Thu%20Apr%2016%202020%2018:00:00%20GMT%2B0200%20(CEST)&utm_content=TechTracking) to be part of a global cohort of tech
|
||||
founders.
|
||||
|
||||
@@ -1329,11 +1329,6 @@ node-forge@^0.10.0:
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
|
||||
|
||||
node-forge@^0.9.1:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.2.tgz#b35a44c28889b2ea55cabf8c79e3563f9676190a"
|
||||
integrity sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==
|
||||
|
||||
npm-run-path@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
||||
|
||||
81
cloud_functions/functions/src/aggregates/index.ts
Normal file
81
cloud_functions/functions/src/aggregates/index.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as functions from "firebase-functions";
|
||||
import * as admin from "firebase-admin";
|
||||
|
||||
import { db } from "../config";
|
||||
import config, { collectionPath } from "../functionConfig";
|
||||
// generated using generateConfig.ts
|
||||
|
||||
const incrementor = (v: number) => admin.firestore.FieldValue.increment(v);
|
||||
const functionConfig: any = config;
|
||||
|
||||
const subDocTrigger = async (
|
||||
change: functions.Change<functions.firestore.DocumentSnapshot>,
|
||||
context: functions.EventContext
|
||||
) => {
|
||||
const beforeData = change.before?.data();
|
||||
const afterData = change.after?.data();
|
||||
const triggerType =
|
||||
Boolean(beforeData) && Boolean(afterData)
|
||||
? "update"
|
||||
: Boolean(afterData)
|
||||
? "create"
|
||||
: "delete";
|
||||
const parentDocRef = change.after
|
||||
? change.after.ref.parent.parent
|
||||
: change.before.ref.parent.parent;
|
||||
const parentDoc = await parentDocRef?.get();
|
||||
const parentDocData = parentDoc?.data();
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
parentDocRef,
|
||||
parentDocData,
|
||||
beforeData,
|
||||
afterData,
|
||||
triggerType,
|
||||
})
|
||||
);
|
||||
//return false;
|
||||
const aggregateData = await functionConfig.reduce(
|
||||
async (accAggregate: any, currAggregate) => {
|
||||
// check relavent sub-table
|
||||
if (currAggregate.subtables.includes(context.params.subCollectionId)) {
|
||||
const newValue = await currAggregate.eval(db)({
|
||||
beforeData,
|
||||
afterData,
|
||||
incrementor,
|
||||
triggerType,
|
||||
});
|
||||
if (newValue !== undefined) {
|
||||
return {
|
||||
...(await accAggregate),
|
||||
...Object.keys(newValue).reduce((acc, curr) => {
|
||||
return {
|
||||
...acc,
|
||||
[`${currAggregate.fieldName}.${curr}`]: newValue[curr],
|
||||
};
|
||||
}, {}),
|
||||
};
|
||||
} else return await accAggregate;
|
||||
} else return await accAggregate;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const update = Object.keys(aggregateData).reduce((acc: any, curr: string) => {
|
||||
if (aggregateData[curr] !== undefined) {
|
||||
return { ...acc, [curr]: aggregateData[curr] };
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, {});
|
||||
console.log({ update, aggregateData });
|
||||
if (parentDocRef && Object.keys(update).length !== 0) {
|
||||
return parentDocRef.update(update);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const FT_aggregates = {
|
||||
[collectionPath.replace("-", "_")]: functions.firestore
|
||||
.document(`${collectionPath}/{parentId}/{subCollectionId}/{docId}`)
|
||||
.onWrite(subDocTrigger),
|
||||
};
|
||||
@@ -1,70 +1,45 @@
|
||||
export default {
|
||||
collectionPath: "feedback",
|
||||
onCreate: true,
|
||||
onUpdate: false,
|
||||
onDelete: false,
|
||||
requiredFields: ["email", "message", "type", "user", "source", "pagePath"],
|
||||
messageDocGenerator: (snapshot) => {
|
||||
const docData = snapshot.data();
|
||||
const { email, message, type, user, source, pagePath } = docData;
|
||||
const { displayName } = user;
|
||||
console.log({ displayName, email, message, type, user, source, pagePath });
|
||||
|
||||
// General/Bug/Idea
|
||||
const generalText =
|
||||
`Hey!, ` +
|
||||
displayName +
|
||||
"(" +
|
||||
email +
|
||||
")" +
|
||||
` has the following general feedback for (` +
|
||||
source.split(".")[0] +
|
||||
`):*` +
|
||||
message +
|
||||
`* `;
|
||||
const bugText =
|
||||
`Bug report: *` +
|
||||
message +
|
||||
`*\n by *` +
|
||||
displayName +
|
||||
"(" +
|
||||
email +
|
||||
")*\n" +
|
||||
"reported on:" +
|
||||
source +
|
||||
pagePath;
|
||||
const ideaText =
|
||||
displayName +
|
||||
"(" +
|
||||
email +
|
||||
")" +
|
||||
"has an amazing idea! for " +
|
||||
source.split(".")[0] +
|
||||
" \n messaged saying:" +
|
||||
message;
|
||||
let text = "blank";
|
||||
switch (type) {
|
||||
case "Bug":
|
||||
text = bugText;
|
||||
break;
|
||||
case "Idea":
|
||||
text = ideaText;
|
||||
break;
|
||||
case "General":
|
||||
text = generalText;
|
||||
break;
|
||||
default:
|
||||
text = "unknown feedback type";
|
||||
}
|
||||
|
||||
if (source.includes("education")) {
|
||||
return {
|
||||
text,
|
||||
emails: ["heath@antler.co", "harini@antler.co", "shams@antler.co"],
|
||||
};
|
||||
} else {
|
||||
return { text, channel: "C01561T4AMB" };
|
||||
}
|
||||
export default [
|
||||
{
|
||||
fieldName: "reviewsSummary",
|
||||
eval: (db) => async ({
|
||||
aggregateState,
|
||||
incrementor,
|
||||
triggerType,
|
||||
change,
|
||||
afterData,
|
||||
beforeData,
|
||||
}) => {
|
||||
//triggerType: create | update | delete
|
||||
//aggregateState: the subtable accumenlator stored in the cell of this column
|
||||
//change: the triggered document change snapshot of the the subcollection
|
||||
//afterData
|
||||
//beforeData
|
||||
//incrementor: short for firebase.firestore.FieldValue.increment(n);
|
||||
//This script needs to return the new aggregateState cell value.
|
||||
switch (triggerType) {
|
||||
case "create":
|
||||
const rating = afterData?.rating ?? 0;
|
||||
return {
|
||||
numberOfReviews: incrementor(1),
|
||||
accumulatedStars: incrementor(rating),
|
||||
};
|
||||
case "update":
|
||||
const prevRating = beforeData?.rating ?? 0;
|
||||
const newRating = afterData?.rating ?? 0;
|
||||
return {
|
||||
accumulatedStars: incrementor(newRating - prevRating),
|
||||
};
|
||||
case "delete":
|
||||
const removeStars = -(beforeData?.rating ?? 0);
|
||||
return {
|
||||
numberOfReviews: incrementor(-1),
|
||||
accumulatedStars: incrementor(removeStars),
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
subtables: ["reviews"],
|
||||
},
|
||||
} as any;
|
||||
export const collectionPath = "";
|
||||
];
|
||||
export const collectionPath = "mvpResources";
|
||||
|
||||
@@ -34,8 +34,6 @@ if (serviceAccount) {
|
||||
const derivativeColumns = Object.values(schemaData.columns).filter(
|
||||
(col: any) => col.type === "DERIVATIVE"
|
||||
);
|
||||
console.log(derivativeColumns);
|
||||
|
||||
const config = derivativeColumns.reduce((acc, currColumn: any) => {
|
||||
return `${acc}{
|
||||
fieldName:'${currColumn.key}',eval:(db)=> async (row) =>{${
|
||||
@@ -48,6 +46,28 @@ if (serviceAccount) {
|
||||
configData = `export default [${config}]\nexport const collectionPath ="${configString}"`;
|
||||
break;
|
||||
|
||||
case "FT_aggregates":
|
||||
const _schemaDoc = await db
|
||||
.doc(`_FIRETABLE_/settings/schema/${configString}`)
|
||||
.get();
|
||||
const _schemaData = _schemaDoc.data();
|
||||
if (!_schemaData) return;
|
||||
const aggregateColumns = Object.values(_schemaData.columns).filter(
|
||||
(col: any) => col.type === "AGGREGATE"
|
||||
);
|
||||
const _config = aggregateColumns.reduce((acc, currColumn: any) => {
|
||||
return `${acc}{
|
||||
fieldName:'${
|
||||
currColumn.key
|
||||
}',eval:(db)=> async ({aggregateState,incrementor,triggerType,change,afterData,beforeData}) =>{${
|
||||
currColumn.config.script
|
||||
}},subtables:[${currColumn.config.subtables
|
||||
.map((t) => `"${t}"`)
|
||||
.join(",")}]},`;
|
||||
}, ``);
|
||||
|
||||
configData = `export default [${_config}]\nexport const collectionPath ="${configString}"`;
|
||||
break;
|
||||
case "FT_subTableStats":
|
||||
configData = `export const collectionPath ="${configString}"\nexport default []`;
|
||||
break;
|
||||
|
||||
@@ -9,6 +9,7 @@ export const callable = callableFns;
|
||||
// all the cloud functions bellow are deployed using the triggerCloudBuild callable function
|
||||
// these functions are designed to be built and deployed based on the configuration passed through the callable
|
||||
export { FT_derivatives } from "./derivatives";
|
||||
export { FT_aggregates } from "./aggregates";
|
||||
export { FT_algolia } from "./algolia";
|
||||
export { FT_sync } from "./collectionSync";
|
||||
export { FT_snapshotSync } from "./snapshotSync";
|
||||
|
||||
@@ -4639,15 +4639,6 @@ node-forge@^0.10.0:
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
|
||||
|
||||
node-forge@^0.7.6:
|
||||
version "0.7.6"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
|
||||
integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
|
||||
|
||||
node-forge@^0.9.0:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.2.tgz#b35a44c28889b2ea55cabf8c79e3563f9676190a"
|
||||
integrity sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==
|
||||
|
||||
noop-logger@^0.1.1:
|
||||
version "0.1.1"
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function Form({ fields, values }: IFormProps) {
|
||||
const { type, ...fieldProps } = field;
|
||||
let _type = type;
|
||||
|
||||
// Derivative field support
|
||||
// Derivative/aggregate field support
|
||||
if (field.config && field.config.renderFieldType) {
|
||||
_type = field.config.renderFieldType;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
|
||||
label="filter template"
|
||||
name="filters"
|
||||
fullWidth
|
||||
value={config.filters}
|
||||
onChange={(e) => {
|
||||
handleChange("filters")(e.target.value);
|
||||
}}
|
||||
@@ -271,6 +272,73 @@ const ConfigFields = ({ fieldType, config, handleChange, tables, columns }) => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
case FieldType.aggregate:
|
||||
return (
|
||||
<>
|
||||
<ColumnSelector
|
||||
label={"Sub Tables"}
|
||||
validTypes={[FieldType.subTable]}
|
||||
value={config.subtables}
|
||||
tableColumns={
|
||||
columns
|
||||
? Array.isArray(columns)
|
||||
? columns
|
||||
: Object.values(columns)
|
||||
: []
|
||||
}
|
||||
handleChange={handleChange("subtables")}
|
||||
/>
|
||||
|
||||
<Typography variant="overline">Aggergate script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={200} />}>
|
||||
<CodeEditor
|
||||
script={
|
||||
config.script ??
|
||||
`//triggerType: create | update | delete\n//aggregateState: the subtable accumenlator stored in the cell of this column\n//snapshot: the triggered document snapshot of the the subcollection\n//incrementor: short for firebase.firestore.FieldValue.increment(n);\n//This script needs to return the new aggregateState cell value.
|
||||
switch (triggerType){
|
||||
case "create":return {
|
||||
count:incrementor(1)
|
||||
}
|
||||
case "update":return {}
|
||||
case "delete":
|
||||
return {
|
||||
count:incrementor(-1)
|
||||
}
|
||||
}`
|
||||
}
|
||||
extraLibs={[
|
||||
` /**
|
||||
* increaments firestore field value
|
||||
*/",
|
||||
function incrementor(value:number):number {
|
||||
|
||||
}`,
|
||||
]}
|
||||
handleChange={handleChange("script")}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Typography variant="overline">Field type of the output</Typography>
|
||||
<FieldsDropdown
|
||||
value={config.renderFieldType}
|
||||
onChange={(newType: any) => {
|
||||
handleChange("renderFieldType")(newType.target.value);
|
||||
}}
|
||||
/>
|
||||
{config.renderFieldType && (
|
||||
<>
|
||||
<Typography variant="overline">Rendered field config</Typography>
|
||||
<ConfigFields
|
||||
fieldType={config.renderFieldType}
|
||||
config={config}
|
||||
handleChange={handleChange}
|
||||
tables={tables}
|
||||
columns={columns}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
case FieldType.derivative:
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -7,10 +7,11 @@ import { setTimeout } from "timers";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
editorWrapper: { position: "relative", minWidth: 800 },
|
||||
editorWrapper: { position: "relative", minWidth: 800, minHeight: 300 },
|
||||
|
||||
editor: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
minHeight: 300,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
resize: "both",
|
||||
fontFamily: "SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace",
|
||||
@@ -31,7 +32,7 @@ const useStyles = makeStyles((theme) =>
|
||||
);
|
||||
|
||||
export default function CodeEditor(props: any) {
|
||||
const { handleChange, script, height = "90hv" } = props;
|
||||
const { handleChange, extraLibs, script, height = 400 } = props;
|
||||
|
||||
const [initialEditorValue] = useState(script ?? "");
|
||||
const { tableState } = useFiretableContext();
|
||||
@@ -84,7 +85,11 @@ export default function CodeEditor(props: any) {
|
||||
allowNonTsExtensions: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (extraLibs) {
|
||||
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
extraLibs.join("\n")
|
||||
);
|
||||
}
|
||||
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
[
|
||||
" /**",
|
||||
@@ -148,7 +153,6 @@ export default function CodeEditor(props: any) {
|
||||
);
|
||||
listenEditorChanges();
|
||||
}, [tableState?.columns]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.editorWrapper}>
|
||||
|
||||
@@ -48,7 +48,6 @@ class TextEditor extends React.Component<
|
||||
getValue() {
|
||||
if ((this.props.column as any).type === FieldType.number)
|
||||
return Number(this.inputRef?.current?.value);
|
||||
|
||||
return this.inputRef?.current?.value;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import ActionIcon from "assets/icons/Action";
|
||||
import JsonIcon from "assets/icons/Json";
|
||||
import CodeIcon from "@material-ui/icons/Code";
|
||||
import DerivativeIcon from "assets/icons/Derivative";
|
||||
import AggregateIcon from "@material-ui/icons/Layers";
|
||||
|
||||
import RichTextIcon from "@material-ui/icons/TextFormat";
|
||||
import ColorIcon from "@material-ui/icons/Colorize";
|
||||
@@ -61,6 +62,7 @@ export {
|
||||
JsonIcon,
|
||||
CodeIcon,
|
||||
DerivativeIcon,
|
||||
AggregateIcon,
|
||||
RichTextIcon,
|
||||
ColorIcon,
|
||||
SliderIcon,
|
||||
@@ -97,6 +99,7 @@ export enum FieldType {
|
||||
json = "JSON",
|
||||
code = "CODE",
|
||||
derivative = "DERIVATIVE",
|
||||
aggregate = "AGGREGATE",
|
||||
|
||||
richText = "RICH_TEXT",
|
||||
color = "COLOR",
|
||||
@@ -148,10 +151,12 @@ export const FIELDS = [
|
||||
type: FieldType.subTable,
|
||||
},
|
||||
|
||||
{ icon: <ActionIcon />, name: "Action", type: FieldType.action },
|
||||
{ icon: <JsonIcon />, name: "JSON", type: FieldType.json },
|
||||
{ icon: <CodeIcon />, name: "Code", type: FieldType.code },
|
||||
|
||||
{ icon: <ActionIcon />, name: "Action", type: FieldType.action },
|
||||
{ icon: <DerivativeIcon />, name: "Derivative", type: FieldType.derivative },
|
||||
{ icon: <AggregateIcon />, name: "Aggregate", type: FieldType.aggregate },
|
||||
|
||||
{ icon: <RichTextIcon />, name: "Rich Text", type: FieldType.richText },
|
||||
{ icon: <ColorIcon />, name: "Color", type: FieldType.color },
|
||||
@@ -178,7 +183,8 @@ export const FIELD_TYPE_DESCRIPTIONS = {
|
||||
[FieldType.duration]: "Duration calculated from two timestamps.",
|
||||
|
||||
[FieldType.url]: "Web address. Firetable does not validate URLs.",
|
||||
[FieldType.rating]: "Rating displayed as stars from 0 to 4.",
|
||||
[FieldType.rating]:
|
||||
"Rating displayed as stars from 0 to configurable number of stars(5 by default).",
|
||||
|
||||
[FieldType.image]:
|
||||
"Image file uploaded to Firebase Storage. Supports JPEG, PNG, SVG, GIF, WebP.",
|
||||
@@ -201,6 +207,8 @@ export const FIELD_TYPE_DESCRIPTIONS = {
|
||||
[FieldType.code]: "Raw code editable with Monaco Editor.",
|
||||
[FieldType.derivative]:
|
||||
"Value derived from the rest of the row’s values. Displayed using any other field type. Requires Cloud Function setup.",
|
||||
[FieldType.aggregate]:
|
||||
"Value aggregated from a specified subcollection of the row. Displayed using any other field type. Requires Cloud Function setup.",
|
||||
|
||||
[FieldType.richText]: "Rich text editor with predefined HTML text styles.",
|
||||
[FieldType.color]: "Visual color picker. Supports Hex, RGBA, HSLA.",
|
||||
|
||||
@@ -9964,14 +9964,10 @@ node-fetch@^2.3.0, node-fetch@^2.6.0:
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
|
||||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||
|
||||
node-forge@0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
|
||||
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
|
||||
|
||||
node-forge@^0.9.0:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
|
||||
node-forge@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.1.0.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
|
||||
integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
|
||||
|
||||
node-int64@^0.4.0:
|
||||
|
||||
Reference in New Issue
Block a user