Merge pull request #345 from AntlerVC/feature/cloud-run

Feature/cloud run
This commit is contained in:
Shams
2021-03-24 00:32:37 +10:00
committed by GitHub
68 changed files with 1885 additions and 7223 deletions

View File

@@ -1,31 +0,0 @@
steps:
- name: node:14.9.0
entrypoint: yarn
args: ["install"]
dir: "FT_functions/compiler"
- name: node:14.9.0
entrypoint: yarn
args:
- "compile"
- "${_SCHEMA_PATH}"
dir: "FT_functions/compiler"
- name: node:14.9.0
entrypoint: yarn
args: ["install"]
dir: "FT_functions/functions"
- name: node:14.9.0
entrypoint: yarn
args:
- "deployFT"
- "--project"
- "${_PROJECT_ID}"
- "--token"
- "${_FIREBASE_TOKEN}"
- "--only"
- "functions"
dir: "FT_functions/functions"
substitutions:
_PROJECT_ID: "project-id" # default value
options:
machineType: "N1_HIGHCPU_8"

View File

@@ -1,66 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
firebase-debug.*.log*
# Firebase cache
.firebase/
# Firebase config
# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env

View File

@@ -1,28 +0,0 @@
import { addPackages, addSparkLib } from "./terminal";
const fs = require("fs");
import { generateConfigFromTableSchema } from "./loader";
async function asyncForEach(array: any[], callback: Function) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
generateConfigFromTableSchema(process.argv[2]).then(async () => {
const configFile = fs.readFileSync(
"../functions/src/functionConfig.ts",
"utf-8"
);
const requiredDependencies = configFile.match(
/(?<=(require\(("|'))).*?(?=("|')\))/g
);
if (requiredDependencies) {
await addPackages(requiredDependencies.map((p) => ({ name: p })));
}
const { sparksConfig } = require("../functions/src/functionConfig");
const requiredSparks = sparksConfig.map((s) => s.type);
console.log({ requiredSparks });
await asyncForEach(requiredSparks, async (s) => await addSparkLib(s));
});

View File

@@ -1,30 +0,0 @@
{
"name": "firetable-functions-compiler",
"scripts": {
"compile": "ts-node index"
},
"engines": {
"node": "14"
},
"main": "lib/index.js",
"dependencies": {
"firebase-admin": "^9.2.0"
},
"devDependencies": {
"@types/node": "^14.14.11",
"firebase-tools": "^8.7.0",
"husky": "^4.2.5",
"js-beautify": "^1.13.5",
"prettier": "^2.1.1",
"pretty-quick": "^3.0.0",
"ts-node": "^8.6.2",
"tslint": "^6.1.0",
"typescript": "^4.1.2"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
},
"private": true
}

View File

@@ -1,39 +0,0 @@
import * as child from "child_process";
function execute(command, callback) {
child.exec(command, function (error, stdout, stderr) {
console.log({ error, stdout, stderr });
callback(stdout);
});
}
export const addPackages = (packages: { name: string; version?: string }[]) =>
new Promise((resolve, reject) => {
//const command =`cd FT_functions/functions;yarn add ${packageName}@${version}`
const packagesString = packages.reduce((acc, currPackage) => {
return `${acc} ${currPackage.name}@${currPackage.version ?? "latest"}`;
}, "");
if (packagesString.trim().length !== 0) {
execute("ls", function () {});
const command = `cd ../functions;yarn add ${packagesString}`;
console.log(command);
execute(command, function () {
resolve(true);
});
} else resolve(false);
});
export const addSparkLib = (name: string) =>
new Promise(async (resolve, reject) => {
const { dependencies } = require(`../sparksLib/${name}`);
const packages = Object.keys(dependencies).map((key) => ({
name: key,
version: dependencies[key],
}));
await addPackages(packages);
const command = `cp ../sparksLib/${name}.ts ../functions/src/sparks/${name}.ts`;
execute(command, function () {
resolve(true);
});
});

View File

@@ -1,64 +0,0 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true,
},
extends: ["plugin:import/errors", "plugin:import/warnings"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: "tsconfig.json",
sourceType: "module",
},
plugins: ["@typescript-eslint", "import"],
rules: {
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/unified-signatures": "warn",
"comma-dangle": "warn",
"constructor-super": "error",
eqeqeq: ["warn", "always"],
"import/no-deprecated": "warn",
"import/no-extraneous-dependencies": "error",
"import/no-unassigned-import": "warn",
"no-cond-assign": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "error",
"no-empty": [
"error",
{
allowEmptyCatch: true,
},
],
"no-invalid-this": "error",
"no-new-wrappers": "error",
"no-param-reassign": "error",
"no-redeclare": "error",
"no-sequences": "error",
"no-shadow": [
"error",
{
hoist: "all",
},
],
"no-throw-literal": "error",
"no-unsafe-finally": "error",
"no-unused-labels": "error",
"no-var": "warn",
"no-void": "error",
"prefer-const": "warn",
},
settings: {
jsdoc: {
tagNamePreference: {
returns: "return",
},
},
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
firebase-debug.*.log*
# Firebase cache
.firebase/
# Firebase config
# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env

View File

@@ -1,8 +0,0 @@
{
"functions": {
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint",
"npm --prefix \"$RESOURCE_DIR\" run build"
]
}
}

View File

@@ -1,68 +0,0 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true,
},
extends: [
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
],
parser: "@typescript-eslint/parser",
parserOptions: {
project: "tsconfig.json",
sourceType: "module",
},
plugins: ["@typescript-eslint", "import"],
rules: {
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/unified-signatures": "warn",
"comma-dangle": ["error", "always-multiline"],
"constructor-super": "error",
eqeqeq: ["warn", "always"],
"import/no-deprecated": "warn",
"import/no-extraneous-dependencies": "error",
"import/no-unassigned-import": "warn",
"no-cond-assign": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "error",
"no-empty": [
"error",
{
allowEmptyCatch: true,
},
],
"no-invalid-this": "error",
"no-new-wrappers": "error",
"no-param-reassign": "error",
"no-redeclare": "error",
"no-sequences": "error",
"no-shadow": [
"error",
{
hoist: "all",
},
],
"no-throw-literal": "error",
"no-unsafe-finally": "error",
"no-unused-labels": "error",
"no-var": "warn",
"no-void": "error",
"prefer-const": "warn",
},
settings: {
jsdoc: {
tagNamePreference: {
returns: "return",
},
},
},
};

View File

@@ -1,12 +0,0 @@
# Compiled JavaScript files
**/*.js
**/*.js.map
# Except the ESLint config file
!.eslintrc.js
# TypeScript v1 declaration files
typings/
# Node.js dependency directory
node_modules/

View File

@@ -1,30 +0,0 @@
{
"name": "functions",
"scripts": {
"lint": "eslint \"src/**/*\"",
"build": "tsc",
"serve": "npm run build && firebase emulators:start --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "12"
},
"main": "lib/index.js",
"dependencies": {
"@google-cloud/cloudbuild": "^2.0.6",
"firebase-admin": "^9.2.0",
"firebase-functions": "^3.11.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^3.9.1",
"@typescript-eslint/parser": "^3.8.0",
"eslint": "^7.6.0",
"eslint-plugin-import": "^2.22.0",
"firebase-functions-test": "^0.2.0",
"typescript": "^3.8.0"
},
"private": true
}

View File

@@ -1,13 +0,0 @@
// Initialize Firebase Admin
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
// Initialize Cloud Firestore Database
export const db = admin.firestore();
// Initialize Auth
export const auth = admin.auth();
const settings = { timestampsInSnapshots: true };
db.settings(settings);
export const env = functions.config();

View File

@@ -1,82 +0,0 @@
import * as functions from "firebase-functions";
import { hasAnyRole } from "./utils/auth";
//import { serverTimestamp } from "./utils";
import { db } from "./firebaseConfig";
const { CloudBuildClient } = require("@google-cloud/cloudbuild");
const cb = new CloudBuildClient();
export const FT_triggerCloudBuild = functions.https.onCall(
async (
data: {
schemaPath: string;
},
context: functions.https.CallableContext
) => {
try {
const authorized = hasAnyRole(["ADMIN"], context);
const { schemaPath } = data;
const firetableSettingsDoc = await db.doc("_FIRETABLE_/settings").get();
const firetableSettings = firetableSettingsDoc.data();
if (!firetableSettings) throw Error("Error: firetableSettings not found");
const { triggerId, branch } = firetableSettings.cloudBuild;
if (!context.auth || !authorized) {
console.warn(`unauthorized user${context}`);
return {
success: false,
message: "you don't have permission to trigger a build",
};
}
// Starts a build against the branch provided.
const [resp] = await cb.runBuildTrigger({
projectId: process.env.GCLOUD_PROJECT, //project hosting cloud build
triggerId,
source: {
branchName: branch,
substitutions: {
_PROJECT_ID: process.env.GCLOUD_PROJECT,
_SCHEMA_PATH: schemaPath,
},
},
});
const buildId = resp.metadata.build.id;
const logUrl = resp.metadata.build.logUrl;
await db.doc(schemaPath).update({ cloudBuild: { logUrl, buildId } });
console.log({ buildId, logUrl });
if (buildId && logUrl) {
return {
message: "Deploying latest configuration",
success: true,
};
}
return false;
} catch (err) {
return {
message: err,
success: false,
};
}
}
);
export const FT_cloudBuildUpdates = functions.pubsub
.topic("cloud-builds")
.onPublish(async (message, context) => {
console.log(JSON.stringify(message));
const { buildId, status } = message.attributes;
console.log(JSON.stringify({ buildId, status }));
//message
//status: "SUCCESS"
//buildId: "1a6d7819-aa35-486c-a29c-fb67eb39430f"
const query = await db
.collection("_FIRETABLE_/settings/schema")
.where("cloudBuild.buildId", "==", buildId)
.get();
if (query.docs.length !== 0) {
await query.docs[0].ref.update({ "cloudBuild.status": status });
}
return true;
});

View File

@@ -1,17 +0,0 @@
import * as functions from "firebase-functions";
export const hasAnyRole = (
authorizedRoles: string[],
context: functions.https.CallableContext
) => {
if (!context.auth || !context.auth.token.roles) return false;
const userRoles = context.auth.token.roles as string[];
const authorization = authorizedRoles.reduce(
(authorized: boolean, role: string) => {
if (userRoles.includes(role)) return true;
else return authorized;
},
false
);
return authorization;
};

View File

@@ -1,57 +0,0 @@
import * as admin from "firebase-admin";
export const serverTimestamp = admin.firestore.FieldValue.serverTimestamp;
// import { sendEmail } from "./email";
// import { hasAnyRole } from "./auth";
// import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
// const secrets = new SecretManagerServiceClient();
// export const getSecret = async (name: string, v: string = "latest") => {
// const [version] = await secrets.accessSecretVersion({
// name: `projects/${process.env.GCLOUD_PROJECT}/secrets/${name}/versions/${v}`,
// });
// const payload = version.payload?.data?.toString();
// if (payload && payload[0] === "{") {
// return JSON.parse(payload);
// } else {
// return payload;
// }
// };
// const characters =
// "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
// export function generateId(length: number): string {
// let result = "";
// const charactersLength = characters.length;
// for (let i = 0; i < length; i++) {
// result += characters.charAt(Math.floor(Math.random() * charactersLength));
// }
// return result;
// }
// export const hasRequiredFields = (requiredFields: string[], data: any) =>
// requiredFields.reduce((acc: boolean, currField: string) => {
// if (data[currField] === undefined || data[currField] === null) return false;
// else return acc;
// }, true);
// async function asyncForEach(array: any[], callback: Function) {
// for (let index = 0; index < array.length; index++) {
// await callback(array[index], index, array);
// }
// }
// export const getTriggerType = (change) =>
// Boolean(change.after.data()) && Boolean(change.before.data())
// ? "update"
// : Boolean(change.after.data())
// ? "create"
// : "delete";
// export default {
// getSecret,
// hasRequiredFields,
// generateId,
// sendEmail,
// serverTimestamp,
// hasAnyRole,
// asyncForEach,
// };

View File

@@ -1,13 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": true,
"outDir": "lib",
"sourceMap": true,
"strict": true,
"target": "es2017"
},
"compileOnSave": true,
"include": ["src"]
}

View File

@@ -324,11 +324,13 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
date-and-time@^0.14.2:
version "0.14.2"
resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.14.2.tgz#a4266c3dead460f6c231fe9674e585908dac354e"
integrity sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA==
debug@4, debug@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"

2
ft_build/. dockerignore Normal file
View File

@@ -0,0 +1,2 @@
antler*.json
.gitignore

1
ft_build/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build/

20
ft_build/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
# Use the official lightweight Node.js image.
# https://hub.docker.com/_/node
FROM node:14-slim
# Create and change to the app directory.
WORKDIR /workdir
# Copy local code to the container image.
# If you've done yarn install locally, node_modules will be copied to
# docker work directory to save time to perform the same actions again.
COPY . ./
# Install production missing dependencies from above copy command.
# If you add a package-lock.json, speed your build by switching to 'npm ci'.
# RUN npm ci --only=production
RUN yarn
# Run the web service on container startup.
CMD [ "yarn", "start" ]

View File

@@ -0,0 +1,63 @@
import { addPackages, addSparkLib, asyncExecute } from "./terminal";
const fs = require("fs");
import { generateConfigFromTableSchema } from "./loader";
import { commandErrorHandler } from "../utils";
const path = require("path");
import admin from "firebase-admin";
export default async function generateConfig(
schemaPath: string,
user: admin.auth.UserRecord
) {
return await generateConfigFromTableSchema(schemaPath, user).then(
async (success) => {
if (!success) {
console.log("generateConfigFromTableSchema failed to complete");
return false;
}
console.log("generateConfigFromTableSchema done");
const configFile = fs.readFileSync(
path.resolve(__dirname, "../functions/src/functionConfig.ts"),
"utf-8"
);
const requiredDependencies = configFile.match(
/(?<=(require\(("|'))).*?(?=("|')\))/g
);
if (requiredDependencies) {
const packgesAdded = await addPackages(
requiredDependencies.map((p: any) => ({ name: p })),
user
);
if (!packgesAdded) {
return false;
}
}
const isFunctionConfigValid = await asyncExecute(
"cd build/functions/src; tsc functionConfig.ts",
commandErrorHandler({
user,
functionConfigTs: configFile,
description: `Invalid compiled functionConfig.ts`,
})
);
if (!isFunctionConfigValid) {
return false;
}
const { sparksConfig } = require("../functions/src/functionConfig.js");
const requiredSparks = sparksConfig.map((s: any) => s.type);
console.log({ requiredSparks });
for (const lib of requiredSparks) {
const success = await addSparkLib(lib, user);
if (!success) {
return false;
}
}
return true;
}
);
}

View File

@@ -1,16 +1,13 @@
import { db } from "../firebaseConfig";
const fs = require("fs");
const beautify = require("js-beautify").js;
// Initialize Firebase Admin
import * as admin from "firebase-admin";
// Initialize Firebase Admin
//const serverTimestamp = admin.firestore.FieldValue.serverTimestamp;
import admin from "firebase-admin";
import { parseSparksConfig } from "../utils";
admin.initializeApp();
//const serviceAccount = require("./antler-vc-firebase.json");
//admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });
const db = admin.firestore();
export const generateConfigFromTableSchema = async (schemaDocPath) => {
export const generateConfigFromTableSchema = async (
schemaDocPath: string,
user: admin.auth.UserRecord
) => {
const schemaDoc = await db.doc(schemaDocPath).get();
const schemaData = schemaDoc.data();
if (!schemaData) throw new Error("no schema found");
@@ -35,7 +32,7 @@ export const generateConfigFromTableSchema = async (schemaDocPath) => {
}',evaluate:async ({row,ref,db,auth,utilFns}) =>{${
currColumn.config.script
}},\nlistenerFields:[${currColumn.config.listenerFields
.map((fieldKey) => `"${fieldKey}"`)
.map((fieldKey: string) => `"${fieldKey}"`)
.join(",\n")}]},\n`;
},
""
@@ -77,13 +74,14 @@ export const generateConfigFromTableSchema = async (schemaDocPath) => {
return `${acc}{\nfieldName:'${
currColumn.key
}',\ntrackedFields:[${currColumn.config.trackedFields
.map((fieldKey) => `"${fieldKey}"`)
.map((fieldKey: string) => `"${fieldKey}"`)
.join(",\n")}]},\n`;
},
""
)}]`;
const sparksConfig = schemaData.sparks ? schemaData.sparks : "[]";
const sparksConfig = parseSparksConfig(schemaData.sparks, user);
const collectionType = schemaDocPath.includes("subTables")
? "subCollection"
: schemaDocPath.includes("groupSchema")
@@ -94,7 +92,7 @@ export const generateConfigFromTableSchema = async (schemaDocPath) => {
let triggerPath = "";
switch (collectionType) {
case "collection":
collectionId = schemaDocPath.split("/").pop();
collectionId = schemaDocPath.split("/").pop() ?? "";
functionName = `"${collectionId}"`;
triggerPath = `"${collectionId}/{docId}"`;
break;
@@ -118,7 +116,7 @@ export const generateConfigFromTableSchema = async (schemaDocPath) => {
'"';
break;
case "groupCollection":
collectionId = schemaDocPath.split("/").pop();
collectionId = schemaDocPath.split("/").pop() ?? "";
const triggerDepth = schemaData.triggerDepth
? schemaData.triggerDepth
: 1;
@@ -134,7 +132,7 @@ export const generateConfigFromTableSchema = async (schemaDocPath) => {
default:
break;
}
const exports = {
const exports: any = {
triggerPath,
functionName: functionName.replace(/-/g, "_"),
derivativesConfig,
@@ -146,8 +144,12 @@ export const generateConfigFromTableSchema = async (schemaDocPath) => {
const fileData = Object.keys(exports).reduce((acc, currKey) => {
return `${acc}\nexport const ${currKey} = ${exports[currKey]}`;
}, ``);
const path = require("path");
fs.writeFileSync(
"../functions/src/functionConfig.ts",
path.resolve(__dirname, "../functions/src/functionConfig.ts"),
beautify(fileData, { indent_size: 2 })
);
return true;
};

View File

@@ -0,0 +1,63 @@
import * as child from "child_process";
import admin from "firebase-admin";
import { commandErrorHandler } from "../utils";
function execute(command: string, callback: any) {
console.log(command);
child.exec(command, function (error, stdout, stderr) {
console.log({ error, stdout, stderr });
callback(stdout);
});
}
export const asyncExecute = async (command: string, callback: any) =>
new Promise(async (resolve, reject) => {
child.exec(command, async function (error, stdout, stderr) {
console.log({ error, stdout, stderr });
await callback(error, stdout, stderr);
resolve(!error);
});
});
export const addPackages = async (
packages: { name: string; version?: string }[],
user: admin.auth.UserRecord
) => {
const packagesString = packages.reduce((acc, currPackage) => {
return `${acc} ${currPackage.name}@${currPackage.version ?? "latest"}`;
}, "");
if (packagesString.trim().length !== 0) {
const success = await asyncExecute(
`cd build/functions;yarn add ${packagesString}`,
commandErrorHandler({
user,
description: "Error adding packages",
})
);
return success;
}
};
export const addSparkLib = async (
name: string,
user: admin.auth.UserRecord
) => {
const { dependencies } = require(`../sparksLib/${name}`);
const packages = Object.keys(dependencies).map((key) => ({
name: key,
version: dependencies[key],
}));
let success = await addPackages(packages, user);
if (!success) {
return false;
}
success = await asyncExecute(
`cp build/sparksLib/${name}.ts build/functions/src/sparks/${name}.ts`,
commandErrorHandler({
user,
description: "Error copying sparksLib",
})
);
return success;
};

View File

@@ -14,5 +14,5 @@
},
"compileOnSave": true,
"include": ["src", "generateConfig.ts"],
"ignore": ["sparks"]
"ignore": ["sparks", "sparksLib"]
}

34
ft_build/deploy.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
helpFunction()
{
echo "Usage: ./deploy.sh --firebase-token [YOUR FIREBASE TOKEN] --project-id [YOUR GCLOUD PROJECT ID]"
exit 0
}
while test $# -gt 0; do
case "$1" in
--firebase-token)
shift
firebase_token=$1
shift
;;
--project-id)
shift
project_id=$1
shift
;;
*)
echo "$1 is not a recognized flag!"
return 1;
;;
esac
done
if [[ -z "$firebase_token" || -z "$project_id" ]];
then
helpFunction
fi
gcloud builds submit --tag gcr.io/$project_id/ft-builder
gcloud run deploy ft-builder --image gcr.io/$project_id/ft-builder --platform managed --memory 4Gi --allow-unauthenticated --set-env-vars="_FIREBASE_TOKEN=$firebase_token,_PROJECT_ID=$project_id"

View File

@@ -0,0 +1,10 @@
// Initialize Firebase Admin
import * as admin from "firebase-admin";
admin.initializeApp();
const db = admin.firestore();
const auth = admin.auth();
db.settings({ timestampsInSnapshots: true, ignoreUndefinedProperties: true });
export { db, admin, auth };

View File

@@ -1,5 +1,6 @@
{
"name": "functions",
"version": "0.0.1",
"scripts": {
"lint": "tslint --project tsconfig.json",
"build": "tsc",
@@ -20,11 +21,11 @@
},
"devDependencies": {
"@types/node": "^14.14.11",
"firebase-tools": "^9.2.2",
"husky": "^4.2.5",
"prettier": "^2.1.1",
"pretty-quick": "^3.0.0",
"ts-node": "^8.6.2",
"tsc": "^1.20150623.0",
"tslint": "^6.1.0",
"typescript": "^4.1.2"
},

View File

@@ -51,7 +51,7 @@ const updateLinks = (
);
return Promise.all([...addPromises, ...removePromises]);
} else {
return false
return false;
}
};
export default function propagate(
@@ -59,16 +59,16 @@ export default function propagate(
config: { fieldName: string; trackedFields: string[] }[],
triggerType: "delete" | "create" | "update"
) {
const promises = []
if (["delete","update"].includes(triggerType)){
const promises = [];
if (["delete", "update"].includes(triggerType)) {
const propagateChangesPromise = propagateChangesOnTrigger(
change,
triggerType
);
promises.push(propagateChangesPromise)
};
if(config.length > 0){
promises.push(propagateChangesPromise);
}
if (config.length > 0) {
if (triggerType === "delete") {
config.forEach((c) =>
promises.push(removeRefsOnTargetDelete(change.before.ref, c.fieldName))

139
ft_build/index.ts Normal file
View File

@@ -0,0 +1,139 @@
const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
import { asyncExecute } from "./compiler/terminal";
import generateConfig from "./compiler";
import { auth } from "./firebaseConfig";
import meta from "./package.json";
import { commandErrorHandler, logErrorToDB } from "./utils";
import firebase from "firebase-admin";
const app = express();
const jsonParser = bodyParser.json();
app.use(cors());
app.get("/", async (req: any, res: any) => {
res.send(`Firetable cloud function builder version ${meta.version}`);
});
app.post("/", jsonParser, async (req: any, res: any) => {
let user: firebase.auth.UserRecord;
const userToken = req?.body?.token;
if (!userToken) {
console.log("missing auth token");
res.send({
success: false,
reason: "missing auth token",
});
return;
}
try {
const decodedToken = await auth.verifyIdToken(userToken);
const uid = decodedToken.uid;
user = await auth.getUser(uid);
const roles = user?.customClaims?.roles;
if (!roles || !Array.isArray(roles) || !roles?.includes("ADMIN")) {
await logErrorToDB({
errorDescription: `user is not admin`,
user,
});
res.send({
success: false,
reason: `user is not admin`,
});
return;
}
console.log("successfully authenticated");
} catch (error) {
await logErrorToDB({
errorDescription: `error verifying auth token: ${error}`,
user,
});
res.send({
success: false,
reason: `error verifying auth token: ${error}`,
});
return;
}
const configPath = req?.body?.configPath;
console.log("configPath:", configPath);
if (!configPath) {
await logErrorToDB({
errorDescription: `Invalid configPath (${configPath})`,
user,
});
res.send({
success: false,
reason: "invalid configPath",
});
}
const success = await generateConfig(configPath, user);
if (!success) {
console.log(`generateConfig failed to complete`);
res.send({
success: false,
reason: `generateConfig failed to complete`,
});
return;
}
console.log("generateConfig done");
let hasEnvError = false;
if (!process.env._FIREBASE_TOKEN) {
await logErrorToDB({
errorDescription: `Invalid env: _FIREBASE_TOKEN (${process.env._FIREBASE_TOKEN})`,
user,
});
hasEnvError = true;
}
if (!process.env._PROJECT_ID) {
await logErrorToDB({
errorDescription: `Invalid env: _PROJECT_ID (${process.env._PROJECT_ID})`,
user,
});
hasEnvError = true;
}
if (hasEnvError) {
res.send({
success: false,
reason: "Invalid env: _FIREBASE_TOKEN or _PROJECT_ID",
});
return;
}
await asyncExecute(
`cd build/functions; \
yarn install`,
commandErrorHandler({ user })
);
await asyncExecute(
`cd build/functions; \
yarn deployFT \
--project ${process.env._PROJECT_ID} \
--token ${process.env._FIREBASE_TOKEN} \
--only functions`,
commandErrorHandler({ user })
);
console.log("build complete");
res.send({
success: true,
});
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(
`Firetable cloud function builder ${meta.version}: listening on port ${port}`
);
});

35
ft_build/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "ft-functions-builder",
"description": "Manages the build and deployment of Firetable cloud functions",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start": "yarn build && node build",
"build": "rm -rf build && tsc --project ./ && cp -r functions build && cp -r sparksLib build",
"deploy": "./deploy.sh"
},
"engines": {
"node": "14"
},
"dependencies": {
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"firebase-admin": "^9.2.0",
"firebase-functions": "^3.11.0",
"safe-eval": "^0.4.1"
},
"devDependencies": {
"@types/express": "^4.17.11",
"@types/node": "^14.14.33",
"firebase-tools": "^8.7.0",
"husky": "^4.2.5",
"js-beautify": "^1.13.0",
"prettier": "^2.1.1",
"pretty-quick": "^3.0.0",
"ts-node": "^9.1.1",
"tslint": "^6.1.0",
"typescript": "^4.2.3"
}
}

20
ft_build/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"rootDir": "./",
"outDir": "./build",
"esModuleInterop": true,
"strict": true,
"noImplicitReturns": true,
"noUnusedLocals": false,
"sourceMap": true,
"noImplicitAny": false,
"resolveJsonModule": true,
"lib": ["ESNext"],
"strictNullChecks": false
},
"compileOnSave": true,
"exclude": ["functions", "build"],
"include": ["*.ts", "firebase.json", "sparksLib"]
}

92
ft_build/utils.ts Normal file
View File

@@ -0,0 +1,92 @@
import { db } from "./firebaseConfig";
import admin from "firebase-admin";
const safeEval = require("safe-eval");
function firetableUser(user: admin.auth.UserRecord) {
return {
displayName: user?.displayName,
email: user?.email,
uid: user?.uid,
emailVerified: user?.emailVerified,
photoURL: user?.photoURL,
timestamp: new Date(),
};
}
async function insertErrorRecordToDB(errorRecord: object) {
await db.collection("_FT_ERRORS").add(errorRecord);
}
function commandErrorHandler(meta: {
user: admin.auth.UserRecord;
description?: string;
functionConfigTs?: string;
sparksConfig?: string;
}) {
return async function (error, stdout, stderr) {
if (!error) {
return;
}
const errorRecord = {
errorType: "commandError",
ranBy: firetableUser(meta.user),
createdAt: admin.firestore.FieldValue.serverTimestamp(),
stdout: stdout ?? "",
stderr: stderr ?? "",
errorStackTrace: error?.stack ?? "",
command: error?.cmd ?? "",
description: meta?.description ?? "",
functionConfigTs: meta?.functionConfigTs ?? "",
sparksConfig: meta?.sparksConfig ?? "",
};
insertErrorRecordToDB(errorRecord);
};
}
async function logErrorToDB(data: {
errorDescription: string;
errorExtraInfo?: string;
errorTraceStack?: string;
user: admin.auth.UserRecord;
sparksConfig?: string;
}) {
console.error(data.errorDescription);
const errorRecord = {
errorType: "codeError",
ranBy: firetableUser(data.user),
description: data.errorDescription,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
sparksConfig: data?.sparksConfig ?? "",
errorExtraInfo: data?.errorExtraInfo ?? "",
errorStackTrace: data?.errorTraceStack ?? "",
};
insertErrorRecordToDB(errorRecord);
}
function parseSparksConfig(
sparks: string | undefined,
user: admin.auth.UserRecord
) {
if (sparks) {
try {
// remove leading "sparks.config(" and trailing ")"
return sparks
.replace(/^(\s*)sparks.config\(/, "")
.replace(/\)(\s*)+$/, "");
} catch (error) {
logErrorToDB({
errorDescription: "Sparks is not wrapped with sparks.config",
errorTraceStack: error.stack,
user,
sparksConfig: sparks,
});
}
}
return "[]";
}
export { commandErrorHandler, logErrorToDB, parseSparksConfig };

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@
"@material-ui/lab": "^4.0.0-alpha.56",
"@material-ui/pickers": "^3.2.10",
"@mdi/js": "^5.8.55",
"@monaco-editor/react": "^3.5.5",
"@monaco-editor/react": "^4.1.0",
"@tinymce/tinymce-react": "^3.4.0",
"algoliasearch": "^4.8.6",
"chroma-js": "^2.1.0",

View File

@@ -1,6 +1,6 @@
import React, { useRef, useMemo, useState } from "react";
import clsx from "clsx";
import Editor, { monaco } from "@monaco-editor/react";
import Editor, { useMonaco } from "@monaco-editor/react";
import { useTheme, createStyles, makeStyles } from "@material-ui/core/styles";
@@ -42,6 +42,7 @@ export default function CodeEditor({
const [initialEditorValue] = useState(value ?? "");
const { tableState } = useFiretableContext();
const classes = useStyles();
const monacoInstance = useMonaco();
const editorRef = useRef<any>();
@@ -49,39 +50,42 @@ export default function CodeEditor({
editorRef.current = editor;
}
function listenEditorChanges() {
setTimeout(() => {
editorRef.current?.onDidChangeModelContent((ev) => {
onChange(editorRef.current.getValue());
});
}, 2000);
}
const themeTransformer = (theme: string) => {
switch (theme) {
case "dark":
return "vs-dark";
default:
return theme;
}
};
useMemo(async () => {
monaco
.init()
.then((monacoInstance) => {
monacoInstance.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
{
noSemanticValidation: true,
noSyntaxValidation: false,
}
);
// compiler options
monacoInstance.languages.typescript.javascriptDefaults.setCompilerOptions(
{
target: monacoInstance.languages.typescript.ScriptTarget.ES5,
allowNonTsExtensions: true,
}
);
})
.catch((error) =>
console.error(
"An error occurred during initialization of Monaco: ",
error
)
if (!monacoInstance) {
// useMonaco returns a monaco instance but initialisation is done asynchronously
// dont execute the logic until the instance is initialised
return;
}
try {
monacoInstance.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
{
noSemanticValidation: true,
noSyntaxValidation: false,
}
);
listenEditorChanges();
// compiler options
monacoInstance.languages.typescript.javascriptDefaults.setCompilerOptions(
{
target: monacoInstance.languages.typescript.ScriptTarget.ES5,
allowNonTsExtensions: true,
}
);
} catch (error) {
console.error(
"An error occurred during initialization of Monaco: ",
error
);
}
}, [tableState?.columns]);
return (
@@ -90,9 +94,9 @@ export default function CodeEditor({
className={clsx(classes.editorWrapper, wrapperProps?.className)}
>
<Editor
theme={theme.palette.type}
theme={themeTransformer(theme.palette.type)}
height={height}
editorDidMount={handleEditorDidMount}
onMount={handleEditorDidMount}
language="javascript"
value={initialEditorValue}
options={{
@@ -100,6 +104,7 @@ export default function CodeEditor({
fontFamily: theme.typography.fontFamilyMono,
...editorOptions,
}}
onChange={onChange as any}
/>
</div>
);

View File

@@ -5,23 +5,10 @@ import { FIELDS } from "@antlerengineering/form-builder";
import HelperText from "../HelperText";
export const settings = () => [
{ type: FIELDS.heading, label: "Cloud build configuration" },
{ type: FIELDS.heading, label: "Cloud Run configuration" },
{
type: FIELDS.text,
name: "cloudBuild.branch",
label: "FT Branch",
//validation: yup.string().required("Required"),
name: "ftBuildUrl",
label: "Cloud Run trigger URL",
},
{
type: FIELDS.description,
description: (
<HelperText>Firetable branch to build cloud functions from</HelperText>
),
},
{
type: FIELDS.text,
name: "cloudBuild.triggerId",
label: "Trigger Id",
//validation: yup.string().required("Required"),
},
];
];

View File

@@ -48,12 +48,13 @@ export default function SettingsDialog({
useEffect(() => {
if (!settingsDocState.loading) {
const cloudBuild = settingsDocState?.doc?.cloudBuild;
setForm(cloudBuild ? { cloudBuild } : FORM_EMPTY_STATE);
const ftBuildUrl = settingsDocState?.doc?.ftBuildUrl;
setForm({ ftBuildUrl });
}
}, [settingsDocState.doc, open]);
const handleSubmit = (values) => {
setForm(values)
settingsDocDispatch({ action: DocActions.update, data: values });
handleClose();
};

View File

@@ -10,7 +10,9 @@ import ErrorBoundary from "components/ErrorBoundary";
import Loading from "components/Loading";
import { useFiretableContext } from "contexts/FiretableContext";
import { triggerCloudBuild } from "../../../../firebase/callables";
import { useSnackContext } from "contexts/SnackContext";
import { db } from "../../../../firebase";
import { useAppContext } from "contexts/AppContext";
import { useConfirmation } from "components/ConfirmationDialog";
import { FieldType } from "constants/fields";
@@ -31,6 +33,8 @@ export default function FieldSettings(props: IMenuModalProps) {
const { requestConfirmation } = useConfirmation();
const { tableState } = useFiretableContext();
const snack = useSnackContext();
const appContext = useAppContext();
const handleChange = (key: string) => (update: any) => {
const updatedConfig = _set({ ...newConfig }, key, update);
@@ -128,10 +132,36 @@ export default function FieldSettings(props: IMenuModalProps) {
confirm: "Deploy",
cancel: "Later",
handleConfirm: async () => {
const response = await triggerCloudBuild(
tableState?.config.tableConfig.path
);
console.log(response);
const settingsDoc = await db
.doc("/_FIRETABLE_/settings")
.get();
const ftBuildUrl = settingsDoc.get("ftBuildUrl");
if (!ftBuildUrl) {
snack.open({
message:
"Cloud Run trigger URL not configured. Configuration guide: https://github.com/AntlerVC/firetable/wiki/Setting-up-cloud-Run-FT-Builder",
variant: "error",
});
}
const userTokenInfo = await appContext?.currentUser?.getIdTokenResult();
const userToken = userTokenInfo?.token;
try {
const response = await fetch(ftBuildUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
configPath: tableState?.config.tableConfig.path,
token: userToken,
}),
});
const data = await response.json();
console.log(data);
} catch (e) {
console.error(e);
}
},
});
}

View File

@@ -4,30 +4,29 @@ import _get from "lodash/get";
import _find from "lodash/find";
import _sortBy from "lodash/sortBy";
import { useConfirmation } from "components/ConfirmationDialog";
import { useSnackContext } from "contexts/SnackContext";
import { db } from "../../../firebase";
import { DialogContentText, Chip } from "@material-ui/core";
import Alert from "@material-ui/lab/Alert";
import TableHeaderButton from "./TableHeaderButton";
import SparkIcon from "@material-ui/icons/OfflineBolt";
import Modal from "components/Modal";
import { useFiretableContext } from "contexts/FiretableContext";
import { useAppContext } from "contexts/AppContext";
import CodeEditor from "../editors/CodeEditor";
// import { SnackContext } from "contexts/SnackContext";
import { useFiretableContext } from "contexts/FiretableContext";
import { triggerCloudBuild } from "firebase/callables";
export default function SparksEditor() {
const snack = useSnackContext();
const { tableState, tableActions } = useFiretableContext();
// const snackContext = useContext(SnackContext);
const appContext = useAppContext();
const { requestConfirmation } = useConfirmation();
const currentSparks = tableState?.config.sparks ?? "";
const [localSparks, setLocalSparks] = useState(currentSparks);
const [open, setOpen] = useState(false);
const [isSparksValid, setIsSparksValid] = useState(false);
const handleClose = () => {
if (currentSparks !== localSparks) {
requestConfirmation({
@@ -51,14 +50,38 @@ export default function SparksEditor() {
confirm: "Deploy",
cancel: "later",
handleConfirm: async () => {
const response = await triggerCloudBuild(
tableState?.config.tableConfig.path
);
console.log(response);
const settingsDoc = await db.doc("/_FIRETABLE_/settings").get();
const ftBuildUrl = settingsDoc.get("ftBuildUrl");
if (!ftBuildUrl) {
snack.open({
message:
"Cloud Run trigger URL not configured. Configuration guide: https://github.com/AntlerVC/firetable/wiki/Setting-up-cloud-Run-FT-Builder",
variant: "error",
});
}
const userTokenInfo = await appContext?.currentUser?.getIdTokenResult();
const userToken = userTokenInfo?.token;
try {
const response = await fetch(ftBuildUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
configPath: tableState?.config.tableConfig.path,
token: userToken,
}),
});
const data = await response.json();
console.log(data);
} catch (e) {
console.error(e);
}
},
});
};
// const cloudBuild = tableState?.config.tableConfig.doc.cloudBuild;
return (
<>
<TableHeaderButton
@@ -97,14 +120,20 @@ export default function SparksEditor() {
handleChange={(newValue) => {
setLocalSparks(newValue);
}}
onValideStatusUpdate={({ isValid }) => {
setIsSparksValid(isValid);
}}
/>
{!isSparksValid &&
"Please resolve all errors before you are able to save."}
</>
}
actions={{
primary: {
children: "Save Changes",
onClick: handleSave,
disabled: localSparks === tableState?.config.sparks,
disabled:
!isSparksValid || localSparks === tableState?.config.sparks,
},
secondary: {
children: "Cancel",

View File

@@ -1,9 +1,8 @@
import React, { useRef, useMemo, useState } from "react";
import { useTheme, createStyles, makeStyles } from "@material-ui/core/styles";
import Editor, { monaco } from "@monaco-editor/react";
import Editor, { useMonaco } from "@monaco-editor/react";
import { useFiretableContext } from "contexts/FiretableContext";
import { FieldType } from "constants/fields";
import { setTimeout } from "timers";
const useStyles = makeStyles((theme) =>
createStyles({
@@ -22,8 +21,15 @@ const useStyles = makeStyles((theme) =>
);
export default function CodeEditor(props: any) {
const { handleChange, extraLibs, script, height = 400 } = props;
const {
handleChange,
extraLibs,
script,
height = 400,
onValideStatusUpdate,
} = props;
const theme = useTheme();
const monacoInstance = useMonaco();
const [initialEditorValue] = useState(script ?? "");
const { tableState } = useFiretableContext();
@@ -35,15 +41,22 @@ export default function CodeEditor(props: any) {
editorRef.current = editor;
}
function listenEditorChanges() {
setTimeout(() => {
editorRef.current?.onDidChangeModelContent((ev) => {
handleChange(editorRef.current.getValue());
});
}, 2000);
}
const themeTransformer = (theme: string) => {
switch (theme) {
case "dark":
return "vs-dark";
default:
return theme;
}
};
useMemo(async () => {
if (!monacoInstance) {
// useMonaco returns a monaco instance but initialisation is done asynchronously
// dont execute the logic until the instance is initialised
return;
}
const firestoreDefsFile = await fetch(
`${process.env.PUBLIC_URL}/firestore.d.ts`
);
@@ -53,107 +66,301 @@ export default function CodeEditor(props: any) {
const firestoreDefs = await firestoreDefsFile.text();
// const firebaseAuthDefs = await firebaseAuthDefsFile.text();
// console.timeLog(firebaseAuthDefs);
monaco
.init()
.then((monacoInstance) => {
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
firestoreDefs
);
// monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
// firebaseAuthDefs
// );
monacoInstance.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
{
noSemanticValidation: true,
noSyntaxValidation: false,
}
);
// compiler options
monacoInstance.languages.typescript.javascriptDefaults.setCompilerOptions(
{
target: monacoInstance.languages.typescript.ScriptTarget.ES5,
allowNonTsExtensions: true,
}
);
if (extraLibs) {
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
extraLibs.join("\n"),
"ts:filename/extraLibs.d.ts"
);
// monaco
// .init()
// .then((monacoInstance) => {
try {
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
firestoreDefs
);
// monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
// firebaseAuthDefs
// );
monacoInstance.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
{
noSemanticValidation: false,
noSyntaxValidation: true,
noSuggestionDiagnostics: true,
}
);
// compiler options
monacoInstance.languages.typescript.javascriptDefaults.setCompilerOptions(
{
target: monacoInstance.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
}
);
if (extraLibs) {
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
[
" /**",
" * utility functions",
" */",
"declare namespace utilFns {",
" /**",
" * Sends out an email through sendGrid",
" */",
`function sendEmail(msg:{from: string,
extraLibs.join("\n"),
"ts:filename/extraLibs.d.ts"
);
}
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
[
" /**",
" * utility functions",
" */",
"declare namespace utilFns {",
" /**",
" * Sends out an email through sendGrid",
" */",
`function sendEmail(msg:{from: string,
templateId:string,
personalizations:{to:string,dynamic_template_data:any}[]}):void {
}`,
"}",
].join("\n"),
"ts:filename/utils.d.ts"
);
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
[
" const db:FirebaseFirestore.Firestore;",
// " const auth:admin.auth;",
"declare class row {",
" /**",
" * Returns the row fields",
" */",
...Object.keys(tableState?.columns!).map((columnKey: string) => {
const column = tableState?.columns[columnKey];
switch (column.type) {
case FieldType.shortText:
case FieldType.longText:
case FieldType.email:
case FieldType.phone:
case FieldType.code:
return `static ${columnKey}:string`;
case FieldType.singleSelect:
const typeString = [
...column.config.options.map((opt) => `"${opt}"`),
// "string",
].join(" | ");
return `static ${columnKey}:${typeString}`;
case FieldType.multiSelect:
return `static ${columnKey}:string[]`;
case FieldType.checkbox:
return `static ${columnKey}:boolean`;
default:
return `static ${columnKey}:any`;
}
}),
"}",
].join("\n"),
"ts:filename/rowFields.d.ts"
);
// monacoInstance.editor.create(wrapper, properties);
})
.catch((error) =>
console.error(
"An error occurred during initialization of Monaco: ",
error
)
"}",
].join("\n"),
"ts:filename/utils.d.ts"
);
listenEditorChanges();
}, [tableState?.columns]);
const rowDefinition = [
...Object.keys(tableState?.columns!).map((columnKey: string) => {
const column = tableState?.columns[columnKey];
switch (column.type) {
case FieldType.shortText:
case FieldType.longText:
case FieldType.email:
case FieldType.phone:
case FieldType.code:
return `${columnKey}:string`;
case FieldType.singleSelect:
const typeString = [
...(column.config?.options?.map((opt) => `"${opt}"`) ?? []),
].join(" | ");
return `${columnKey}:${typeString}`;
case FieldType.multiSelect:
return `${columnKey}:string[]`;
case FieldType.checkbox:
return `${columnKey}:boolean`;
default:
return `${columnKey}:any`;
}
}),
].join(";\n");
const availableFields = Object.keys(tableState?.columns!)
.map((columnKey: string) => `"${columnKey}"`)
.join("|\n");
const sparksDefinition = `declare namespace sparks {
// basic types that are used in all places
type Row = {${rowDefinition}};
type Field = ${availableFields} | string | object;
type Fields = Field[];
type Trigger = "create" | "update" | "delete";
type Triggers = Trigger[];
// the argument that the spark body takes in
type SparkContext = {
row: Row;
ref: any;
db: any;
change: any;
triggerType: Triggers;
sparkConfig: any;
}
// function types that defines spark body and shuold run
type ShouldRun = boolean | ((data: SparkContext) => boolean | Promise<any>);
type ContextToString = ((data: SparkContext) => string | Promise<any>);
type ContextToStringList = ((data: SparkContext) => string[] | Promise<any>);
type ContextToObject = ((data: SparkContext) => object | Promise<any>);
type ContextToObjectList = ((data: SparkContext) => object[] | Promise<any>);
type ContextToRow = ((data: SparkContext) => Row | Promise<any>);
type ContextToAny = ((data: SparkContext) => any | Promise<any>);
// different types of bodies that slack message can use
type slackEmailBody = {
channels?: ContextToStringList;
text?: ContextToString;
emails: ContextToStringList;
blocks?: ContextToObjectList;
attachments?: ContextToAny;
}
type slackChannelBody = {
channels: ContextToStringList;
text?: ContextToString;
emails?: ContextToStringList;
blocks?: ContextToObjectList;
attachments?: ContextToAny;
}
// different types of sparks
type docSync = {
type: "docSync";
triggers: Triggers;
shouldRun: ShouldRun;
requiredFields?: Fields;
sparkBody: {
fieldsToSync: Fields;
row: ContextToRow;
targetPath: ContextToString;
}
};
type historySnapshot = {
type: "historySnapshot";
triggers: Triggers;
shouldRun: ShouldRun;
sparkBody: {
trackedFields: Fields;
}
}
type algoliaIndex = {
type: "algoliaIndex";
triggers: Triggers;
shouldRun: ShouldRun;
requiredFields?: Fields;
sparkBody: {
fieldsToSync: Fields;
index: string;
row: ContextToRow;
objectID: ContextToString;
}
}
type slackMessage = {
type: "slackMessage";
triggers: Triggers;
shouldRun: ShouldRun;
requiredFields?: Fields;
sparkBody: slackEmailBody | slackChannelBody;
}
type sendgridEmail = {
type: "sendgridEmail";
triggers: Triggers;
shouldRun: ShouldRun;
requiredFields?: Fields;
sparkBody: {
msg: ContextToAny;
}
}
type apiCall = {
type: "apiCall";
triggers: Triggers;
shouldRun: ShouldRun;
requiredFields?: Fields;
sparkBody: {
body: ContextToString;
url: ContextToString;
method: ContextToString;
callback: ContextToAny;
}
}
type twilioMessage = {
type: "twilioMessage";
triggers: Triggers;
shouldRun: ShouldRun;
requiredFields?: Fields;
sparkBody: {
body: ContextToAny;
from: ContextToAny;
to: ContextToAny;
}
}
// an individual spark
type Spark =
| docSync
| historySnapshot
| algoliaIndex
| slackMessage
| sendgridEmail
| apiCall
| twilioMessage;
type Sparks = Spark[]
// use spark.config(sparks) in the code editor for static type check
function config(sparks: Sparks): void;
}`;
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
[
" /**",
" * sparks type configuration",
" */",
sparksDefinition,
// "interface sparks {",
// " /**",
// " * define your sparks for current table inside sparks.config",
// " */",
// `config(spark: object[]):void;`,
// "}",
].join("\n"),
"ts:filename/sparks.d.ts"
);
monacoInstance.languages.typescript.javascriptDefaults.addExtraLib(
[
" const db:FirebaseFirestore.Firestore;",
// " const auth:admin.auth;",
"declare class row {",
" /**",
" * Returns the row fields",
" */",
...Object.keys(tableState?.columns!).map((columnKey: string) => {
const column = tableState?.columns[columnKey];
switch (column.type) {
case FieldType.shortText:
case FieldType.longText:
case FieldType.email:
case FieldType.phone:
case FieldType.code:
return `static ${columnKey}:string`;
case FieldType.singleSelect:
const typeString = [
...(column.config?.options?.map((opt) => `"${opt}"`) ?? []),
// "string",
].join(" | ");
return `static ${columnKey}:${typeString}`;
case FieldType.multiSelect:
return `static ${columnKey}:string[]`;
case FieldType.checkbox:
return `static ${columnKey}:boolean`;
default:
return `static ${columnKey}:any`;
}
}),
"}",
].join("\n"),
"ts:filename/rowFields.d.ts"
);
} catch (error) {
console.error(
"An error occurred during initialization of Monaco: ",
error
);
}
}, [tableState?.columns, monacoInstance]);
function handleEditorValidation(markers) {
if (onValideStatusUpdate) {
onValideStatusUpdate({
isValid: markers.length <= 0,
});
}
}
return (
<>
<div className={classes.editorWrapper}>
<Editor
theme={theme.palette.type}
theme={themeTransformer(theme.palette.type)}
height={height}
editorDidMount={handleEditorDidMount}
onMount={handleEditorDidMount}
language="javascript"
value={initialEditorValue}
onChange={handleChange}
onValidate={handleEditorValidation}
/>
</div>
</>

View File

@@ -2,7 +2,6 @@ import { functions } from "./index";
export enum CLOUD_FUNCTIONS {
ImpersonatorAuth = "callable-ImpersonatorAuth",
triggerCloudBuild = "FT_triggerCloudBuild",
}
export const cloudFunction = (
@@ -28,6 +27,3 @@ export const cloudFunction = (
export const ImpersonatorAuth = (email: string) =>
functions.httpsCallable(CLOUD_FUNCTIONS.ImpersonatorAuth)({ email });
export const triggerCloudBuild = (schemaPath: string) =>
functions.httpsCallable(CLOUD_FUNCTIONS.triggerCloudBuild)({ schemaPath });

View File

@@ -1493,13 +1493,6 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.11.0":
version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.8.3":
version "7.10.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c"
@@ -1662,10 +1655,10 @@
resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.10.1.tgz#7815e71c9c6f072034415524b29ca8f1d1770660"
integrity sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==
"@firebase/auth@0.14.9":
version "0.14.9"
resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.14.9.tgz#481db24d5bd6eded8ac2e5aea6edb9307040229c"
integrity sha512-PxYa2r5qUEdheXTvqROFrMstK8W4uPiP7NVfp+2Bec+AjY5PxZapCx/YFDLkU0D7YBI82H74PtZrzdJZw7TJ4w==
"@firebase/auth@0.15.0":
version "0.15.0"
resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.15.0.tgz#45d6def6d6d9444432c005710df442991828275f"
integrity sha512-IFuzhxS+HtOQl7+SZ/Mhaghy/zTU7CENsJFWbC16tv2wfLZbayKF5jYGdAU3VFLehgC8KjlcIWd10akc3XivfQ==
dependencies:
"@firebase/auth-types" "0.10.1"
@@ -1697,21 +1690,21 @@
faye-websocket "0.11.3"
tslib "^1.11.1"
"@firebase/firestore-types@1.13.0":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.13.0.tgz#4ab9c40e1e66e8193a929460d64507acd07d9230"
integrity sha512-QF5CAuYOHE6Zbsn1uEg6wkl836iP+i6C0C/Zs3kF60eebxZvTWp8JSZk19Ar+jj4w+ye8/7H5olu5CqDNjWpEA==
"@firebase/firestore-types@1.14.0":
version "1.14.0"
resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.14.0.tgz#4516249d3c181849fd3c856831944dbd5c8c55fc"
integrity sha512-WF8IBwHzZDhwyOgQnmB0pheVrLNP78A8PGxk1nxb/Nrgh1amo4/zYvFMGgSsTeaQK37xMYS/g7eS948te/dJxw==
"@firebase/firestore@1.17.3":
version "1.17.3"
resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-1.17.3.tgz#512d9c1afdba4690aa62de0f53276cf0abbe5f51"
integrity sha512-wRdrgeSBJ50eo63x8GnO8NgVNe3vBw2xhKhyMXl0JTWQIbxnlMjAHcz7b85VvsqPLI7U70PgWQnfQtJOXRCNUA==
"@firebase/firestore@1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-1.18.0.tgz#3430e8c60d3e6be1d174b3a258838b1944c93a4d"
integrity sha512-maMq4ltkrwjDRusR2nt0qS4wldHQMp+0IDSfXIjC+SNmjnWY/t/+Skn9U3Po+dB38xpz3i7nsKbs+8utpDnPSw==
dependencies:
"@firebase/component" "0.1.19"
"@firebase/firestore-types" "1.13.0"
"@firebase/firestore-types" "1.14.0"
"@firebase/logger" "0.2.6"
"@firebase/util" "0.3.2"
"@firebase/webchannel-wrapper" "0.3.0"
"@firebase/webchannel-wrapper" "0.4.0"
"@grpc/grpc-js" "^1.0.0"
"@grpc/proto-loader" "^0.5.0"
node-fetch "2.6.1"
@@ -1836,10 +1829,10 @@
dependencies:
tslib "^1.11.1"
"@firebase/webchannel-wrapper@0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.3.0.tgz#d1689566b94c25423d1fb2cb031c5c2ea4c9f939"
integrity sha512-VniCGPIgSGNEgOkh5phb3iKmSGIzcwrccy3IomMFRWPCMiCk2y98UQNJEoDs1yIHtZMstVjYWKYxnunIGzC5UQ==
"@firebase/webchannel-wrapper@0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.4.0.tgz#becce788818d3f47f0ac1a74c3c061ac1dcf4f6d"
integrity sha512-8cUA/mg0S+BxIZ72TdZRsXKBP5n5uRcE3k29TZhZw6oIiHBt9JA7CTb/4pE1uKtE/q5NeTY2tBDcagoZ+1zjXQ==
"@google-cloud/paginator@^2.0.0":
version "2.0.3"
@@ -2224,12 +2217,21 @@
resolved "https://registry.yarnpkg.com/@mdi/js/-/js-5.8.55.tgz#630bc5fafd8b1d2f6e63489a9ab170177559e41b"
integrity sha512-2bvln56SW6V/nSDC/0/NTu1bMF/CgSyZox8mcWbAPWElBN3UYIrukKDUckEER8ifr8X2YJl1RLKQqi7T7qLzmg==
"@monaco-editor/react@^3.5.5":
version "3.5.5"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-3.5.5.tgz#7ebef1903bf7e7925084f16efb6550a83a7efed6"
integrity sha512-2/j9KFMu0Lr1DKX6ZqPwiCbhfv47yD8849Cyi8BTlGGRIzCcrF2pIQPbsZrsO+A+uG/352BfOsSF3zKQT84NQQ==
"@monaco-editor/loader@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.0.1.tgz#7068c9b07bbc65387c0e7a4df6dac0a326155905"
integrity sha512-hycGOhLqLYjnD0A/FHs56covEQWnDFrSnm/qLKkB/yoeayQ7ju+Vaj4SdTojGrXeY6jhMDx59map0+Jqwquh1Q==
dependencies:
"@babel/runtime" "^7.11.0"
state-local "^1.0.6"
"@monaco-editor/react@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.1.0.tgz#9a362a4e0973c9958574551984a3c3eeeff06e5f"
integrity sha512-Hh895v/KfGgckDLXq8sdDGT4xS89+2hbQOP1l57sLd2XlJycChdzPiCj02nQDIduLmUIVHittjaj1/xmy94C3A==
dependencies:
"@monaco-editor/loader" "^1.0.1"
prop-types "^15.7.2"
state-local "^1.0.7"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
@@ -6761,16 +6763,16 @@ firebase-tools@^8.12.1:
ws "^7.2.3"
firebase@^7.23.0:
version "7.23.0"
resolved "https://registry.yarnpkg.com/firebase/-/firebase-7.23.0.tgz#d58bf936bd9f3a00717d81854280747222d2803b"
integrity sha512-0b1zi0H8jT4KqyPabldzPhyKTeptw5E5a7KkjWW3MBMVV/LjbC6/NKhRR8sGQNbsbS2LnTvyEENWbqkZP2ZXtw==
version "7.24.0"
resolved "https://registry.yarnpkg.com/firebase/-/firebase-7.24.0.tgz#dab53b9c0f1c9538d2d6f4f51769897b0b6d60d8"
integrity sha512-j6jIyGFFBlwWAmrlUg9HyQ/x+YpsPkc/TTkbTyeLwwAJrpAmmEHNPT6O9xtAnMV4g7d3RqLL/u9//aZlbY4rQA==
dependencies:
"@firebase/analytics" "0.6.0"
"@firebase/app" "0.6.11"
"@firebase/app-types" "0.6.1"
"@firebase/auth" "0.14.9"
"@firebase/auth" "0.15.0"
"@firebase/database" "0.6.13"
"@firebase/firestore" "1.17.3"
"@firebase/firestore" "1.18.0"
"@firebase/functions" "0.5.1"
"@firebase/installations" "0.4.17"
"@firebase/messaging" "0.7.1"
@@ -13804,6 +13806,11 @@ stack-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
state-local@^1.0.6, state-local@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"