1 Commits

Author SHA1 Message Date
Kurt Hutten
a78196b496 Github app start 2021-05-06 20:07:24 +10:00
320 changed files with 9605 additions and 19189 deletions

3
.gitmodules vendored
View File

@@ -0,0 +1,3 @@
[submodule "web/src/cascade"]
path = app/web/src/cascade
url = https://github.com/Irev-Dev/CascadeStudio.git

11
.vscode/settings.json vendored
View File

@@ -1,11 +0,0 @@
{
"cSpell.words": [
"Cadhub",
"Customizer",
"Hutten",
"cadquery",
"jscad",
"openscad",
"sendmail"
]
}

View File

@@ -2,27 +2,24 @@
# [C a d H u b](https://cadhub.xyz)
<!-- [![Netlify Status](https://api.netlify.com/api/v1/badges/77f37543-e54a-4723-8136-157c0221ec27/deploy-status)](https://app.netlify.com/sites/cadhubxyz/deploys) -->
[![Netlify Status](https://api.netlify.com/api/v1/badges/77f37543-e54a-4723-8136-157c0221ec27/deploy-status)](https://app.netlify.com/sites/cadhubxyz/deploys)
Let's help Code-CAD reach its [full potential!](https://cadhub.xyz) We're making a ~~cad~~hub for the Code-CAD community, think of it as model-repository crossed with a live editor. We have integrations in progress for [OpenSCAD](https://cadhub.xyz/dev-ide/openscad) and [CadQuery](https://cadhub.xyz/dev-ide/cadquery) with [more coming soon](https://github.com/Irev-Dev/curated-code-cad).
Let's help Code-CAD reach its [full potential!](https://cadhub.xyz) We're making a ~~cad~~hub for the Code-CAD community, think of it as model-repository crossed with a live editor. We have an integration with the excellent [cascadeStudio](https://zalo.github.io/CascadeStudio/) with [more coming soon](https://github.com/Irev-Dev/curated-code-cad).
If you want to be involved in anyway, checkout the [Road Map](https://github.com/Irev-Dev/cadhub/discussions/212) and get in touch via, [twitter](https://twitter.com/IrevDev), [discord](https://discord.gg/SD7zFRNjGH) or [discussions](https://github.com/Irev-Dev/cadhub/discussions).
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/fullcadhubshot.jpg">
<img src="https://raw.githubusercontent.com/Irev-Dev/cadhub/main/docs/static/img/blog/curated-code-cad/CadHubSS.jpg">
## Getting your dev environment setup
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/Part%20IDE%20-%20export%20expand%20state.jpg">
## Getting Started
Because we're integrating cascadeStudio, this is done some what crudely for the time being, so you'll need to clone the repo with submodules.
```terminal
git clone git@github.com:Irev-Dev/cadhub.git
git clone --recurse-submodules -j8 git@github.com:Irev-Dev/cadhub.git
# or
git clone https://github.com/Irev-Dev/cadhub.git
```
cd in the app directory
```
cd app
git clone --recurse-submodules -j8 https://github.com/Irev-Dev/cadhub.git
```
Install dependencies
@@ -30,7 +27,10 @@ Install dependencies
yarn install
```
Setting up the db, you'll need to have a postgres installed locally, you can [follow this guide](https://redwoodjs.com/docs/local-postgres-setup).
Setting up the db, you'll need to have a postgres installed locally, you can [follow this guide](https://redwoodjs.com/docs/local-postgres-setup) with a couple of exceptions:
- Run `yarn rw prisma migrate dev` instead of `yarn rw db up` in the guide.
- Don't worry about changing the `schema.prisma` file.
- You will need to add a `DATABASE_URL` and test url to you `.env` file at the root of the project.
Run the following
``` terminal
@@ -38,8 +38,6 @@ yarn rw prisma migrate dev
yarn rw prisma db seed
```
p.s. `yarn rw prisma studio` spins up an app to inspect the db
### Fire up dev
```terminal
yarn rw dev
@@ -47,8 +45,6 @@ yarn rw dev
Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`.
If you want to access the websight on your phone use `yarn redwood dev --fwd="--host <ip-address-on-your-network-i.e.-192.168.0.5">"`
you can sign in to the following accounts locally
localUser1@kurthutten.com: `abc123`
@@ -77,13 +73,6 @@ We're using tailwind utility classes so please try and use them as much as possi
In progress, though can be [seen on Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=0%3A1)
## Integrations
The OpenSCAD and CadQuery integrations work by leveraging each of their cli tools in a docker image. It's currently deployed to AWS and can be found [here](https://github.com/Irev-Dev/cadhub/tree/main/app/api/src/docker).
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/Part%20Page(1).jpg">
## Docs
Docs are hosted at [learn.cadhub.xyz](http://learn.cadhub.xyz/). It includes a OpenSCAD tutorial at this point, and more is coming. The docs can be found in this repo at [docs](https://github.com/Irev-Dev/cadhub/tree/main/docs)
## Who is CadHub
[Kurt](https://github.com/Irev-Dev) and [Frank](https://github.com/franknoirot) make up the Core-team and [Jeremy](https://github.com/jmwright) is a major contributor. Plus a number smaller contributors.
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/User%20Page%20Edit.jpg">

View File

@@ -18,13 +18,8 @@ CLOUDINARY_API_KEY=476712943135152
# trace | info | debug | warn | error | silent
# LOG_LEVEL=debug
CAD_LAMBDA_BASE_URL="https://wzab9s632b.execute-api.us-east-1.amazonaws.com/prod"
# EMAIL_PASSWORD=abc123
# CAD_LAMBDA_BASE_URL="http://localhost:8080"
# sentry
GITHUB_ASSIST_APP_ID=23342
GITHUB_ASSIST_SECRET=abc
GITHUB_ASSIST_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nabcdefg\n-----END RSA PRIVATE KEY-----"
# Github assist app
# GITHUB_ASSIST_APP_ID=""
# GITHUB_ASSIST_SECRET=""

View File

@@ -1 +1 @@
/web/src/cascade/*

3
app/.gitignore vendored
View File

@@ -1,3 +1,2 @@
dist
web/types/graphql.d.ts
api/types/graphql.d.ts
*.pem

View File

@@ -22,7 +22,9 @@
"Uploader",
"describedby",
"initialise",
"octokit",
"redwoodjs",
"repos",
"resizer",
"roboto",
"ropa"

View File

@@ -1,9 +0,0 @@
-- CreateTable
CREATE TABLE "RW_DataMigration" (
"version" TEXT NOT NULL,
"name" TEXT NOT NULL,
"startedAt" TIMESTAMP(3) NOT NULL,
"finishedAt" TIMESTAMP(3) NOT NULL,
PRIMARY KEY ("version")
);

View File

@@ -1,31 +0,0 @@
/*
Warnings:
- You are about to drop the `Comment` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Part` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `PartReaction` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_partId_fkey";
-- DropForeignKey
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_userId_fkey";
-- DropForeignKey
ALTER TABLE "Part" DROP CONSTRAINT "Part_userId_fkey";
-- DropForeignKey
ALTER TABLE "PartReaction" DROP CONSTRAINT "PartReaction_partId_fkey";
-- DropForeignKey
ALTER TABLE "PartReaction" DROP CONSTRAINT "PartReaction_userId_fkey";
-- DropTable
DROP TABLE "Comment";
-- DropTable
DROP TABLE "Part";
-- DropTable
DROP TABLE "PartReaction";

View File

@@ -1,59 +0,0 @@
-- CreateTable
CREATE TABLE "Project" (
"id" TEXT NOT NULL,
"title" VARCHAR(25) NOT NULL,
"description" TEXT,
"code" TEXT,
"mainImage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"userId" TEXT NOT NULL,
"deleted" BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProjectReaction" (
"id" TEXT NOT NULL,
"emote" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Comment" (
"id" TEXT NOT NULL,
"text" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Project.title_userId_unique" ON "Project"("title", "userId");
-- CreateIndex
CREATE UNIQUE INDEX "ProjectReaction.emote_userId_projectId_unique" ON "ProjectReaction"("emote", "userId", "projectId");
-- AddForeignKey
ALTER TABLE "Project" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProjectReaction" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProjectReaction" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,5 +0,0 @@
-- CreateEnum
CREATE TYPE "CadPackage" AS ENUM ('openscad', 'cadquery');
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "cadPackage" "CadPackage" NOT NULL DEFAULT E'openscad';

View File

@@ -1,17 +0,0 @@
-- CreateTable
CREATE TABLE "SocialCard" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"url" TEXT,
"outOfDate" BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "SocialCard_projectId_unique" ON "SocialCard"("projectId");
-- AddForeignKey
ALTER TABLE "SocialCard" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterIndex
ALTER INDEX "SocialCard_projectId_unique" RENAME TO "SocialCard.projectId_unique";

View File

@@ -1,3 +1,2 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@@ -1,4 +1,4 @@
datasource db {
datasource DS {
provider = "postgresql"
url = env("DATABASE_URL")
}
@@ -14,7 +14,8 @@ generator client {
// ADMIN
// }
// enum ProjectType {
// enum PartType {
// CASCADESTUDIO
// JSCAD
// }
@@ -32,21 +33,15 @@ model User {
image String? // url maybe id or file storage service? cloudinary?
bio String? //mark down
Project Project[]
Reaction ProjectReaction[]
Part Part[]
Reaction PartReaction[]
Comment Comment[]
SubjectAccessRequest SubjectAccessRequest[]
}
enum CadPackage {
openscad
cadquery
// jscad // TODO #422, add jscad to db schema when were ready to enable saving of jscad projects
}
model Project {
model Part {
id String @id @default(uuid())
title String @db.VarChar(25)
title String
description String? // markdown string
code String?
mainImage String? // link to cloudinary
@@ -55,36 +50,23 @@ model Project {
user User @relation(fields: [userId], references: [id])
userId String
deleted Boolean @default(false)
cadPackage CadPackage @default(openscad)
socialCard SocialCard?
Comment Comment[]
Reaction ProjectReaction[]
Reaction PartReaction[]
@@unique([title, userId])
}
model SocialCard {
id String @id @default(uuid())
projectId String @unique
project Project @relation(fields: [projectId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
url String? // link to cloudinary
outOfDate Boolean @default(true)
}
model ProjectReaction {
model PartReaction {
id String @id @default(uuid())
emote String // an emoji
user User @relation(fields: [userId], references: [id])
userId String
project Project @relation(fields: [projectId], references: [id])
projectId String
part Part @relation(fields: [partId], references: [id])
partId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([emote, userId, projectId])
@@unique([emote, userId, partId])
}
model Comment {
@@ -92,8 +74,8 @@ model Comment {
text String // the comment, should I allow mark down?
user User @relation(fields: [userId], references: [id])
userId String
project Project @relation(fields: [projectId], references: [id])
projectId String
part Part @relation(fields: [partId], references: [id])
partId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -109,10 +91,3 @@ model SubjectAccessRequest {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model RW_DataMigration {
version String @id
name String
startedAt DateTime
finishedAt DateTime
}

View File

@@ -50,9 +50,9 @@ async function main() {
})
}
const projects = [
const parts = [
{
title: 'demo-project1',
title: 'demo-part1',
description: '# can be markdown',
mainImage: 'CadHub/kjdlgjnu0xmwksia7xox',
user: {
@@ -62,7 +62,7 @@ async function main() {
},
},
{
title: 'demo-project2',
title: 'demo-part2',
description: '## [hey](www.google.com)',
user: {
connect: {
@@ -72,43 +72,39 @@ async function main() {
},
]
existing = await db.project.findMany({where: { title: projects[0].title}})
existing = await db.part.findMany({where: { title: parts[0].title}})
if(!existing.length) {
await db.project.create({
data: projects[0],
await db.part.create({
data: parts[0],
})
}
existing = await db.project.findMany({where: { title: projects[1].title}})
existing = await db.part.findMany({where: { title: parts[1].title}})
if(!existing.length) {
await db.project.create({
data: projects[1],
await db.part.create({
data: parts[1],
})
}
const aProject = await db.project.findUnique({where: {
const aPart = await db.part.findUnique({where: {
title_userId: {
title: projects[0].title,
title: parts[0].title,
userId: users[0].id,
}
}})
await db.comment.create({
data: {
text: "nice project, I like it",
userId: users[0].id,
projectId: aProject.id,
// user: {connect: { id: users[0].id}},
// project: {connect: { id: aProject.id}},
text: "nice part, I like it",
user: {connect: { id: users[0].id}},
part: {connect: { id: aPart.id}},
}
})
await db.projectReaction.create({
await db.partReaction.create({
data: {
emote: "❤️",
userId: users[0].id,
projectId: aProject.id,
// user: {connect: { id: users[0].id}},
// project: {connect: { id: aProject.id}},
user: {connect: { id: users[0].id}},
part: {connect: { id: aPart.id}},
}
})

View File

@@ -1 +1,6 @@
module.exports = require('@redwoodjs/testing/config/jest/api')
const { getConfig } = require('@redwoodjs/core')
const config = getConfig({ type: 'jest', target: 'node' })
config.displayName.name = 'api'
module.exports = config

View File

@@ -3,13 +3,10 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@redwoodjs/api": "^0.34.1",
"@sentry/node": "^6.5.1",
"cloudinary": "^1.23.0",
"human-id": "^2.0.1",
"nodemailer": "^6.6.2"
},
"devDependencies": {
"@types/nodemailer": "^6.4.2"
"@octokit/app": "^12.0.2",
"@octokit/webhooks-types": "^3.71.1",
"@redwoodjs/api": "^0.31.0",
"@redwoodjs/api-server": "^0.31.0",
"cloudinary": "^1.23.0"
}
}

View File

@@ -1,5 +0,0 @@
# The following are the env vars you need run the cad lamdas locally
# The still connect to s3 so some secrets are needed, ask Kurt and he'll set things up for you
DEV_AWS_SECRET_ACCESS_KEY=""
DEV_AWS_ACCESS_KEY_ID=""
DEV_BUCKET="cad-preview-bucket-dev-001"

View File

@@ -3,7 +3,7 @@
We're using the serverless from work for deployment
```
sls deploy --stage stagename
sls deploy
```
But [Kurt Hutten](https://github.com/Irev-Dev) is the only one with credentials for deployment atm, though if you wanted to set your own account you could deploy to that if you wanted to test.
@@ -14,10 +14,6 @@ You'll need to have Docker installed
Because of the way the docker containers to be deployed as lambdas on aws are somewhat specialised for the purpose we're using `docker-compose` to spin one up for each function/endpoint. So we've added a aws-emulation layer
The docker build relies on a git ignored file, the aws-lambda-rie. [Download it](https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie), then put it into `app/api/src/docker/common/`. alternatively you can put this download into the DockerFiles by reading the instructions at around line 29 of the DockerFiles (`app/api/src/docker/openscad/Dockerfile` & `app/api/src/docker/cadquery/Dockerfile`). However this will mean slower build times as it will need download this 14mb file every build.
you will also need to create a .env in `app/api/src/docker/.env` for the following env-vars `DEV_AWS_SECRET_ACCESS_KEY, DEV_AWS_ACCESS_KEY_ID and DEV_BUCKET`. Ask @irev-dev for credentials and he can sort you out.
Then cd into this folder `cd api/src/docker` and:
```bash
@@ -30,7 +26,8 @@ After which we'll also spin up a light express server to act as an emulator to t
yarn install
yarn emulate
```
You can now add CAD_LAMBDA_BASE_URL="http://localhost:8080" to you .env file and restart your main dev process (`yarn rw dev`) comment that line out if you want to go back to using the aws endpoint (and restart the dev process).
You can now add OPENSCAD_BASE_URL="http://localhost:8080" to you .env file and restart your main dev process (`yarn rw dev`)
comment that line out if you want to go back to using the aws endpoint (and restart the dev process).
If you change anything in the `api/src/docker/openscad` directory, you will need to stop the docker process and restart it (will be fairly quick if you're only changing the js)

View File

@@ -10,26 +10,31 @@ app.use(cors())
const invocationURL = (port) =>
`http://localhost:${port}/2015-03-31/functions/function/invocations`
const makeRequest = (route, port) => [
route,
async (req, res) => {
console.log(`making post request to ${port}, ${route}`)
try {
const { data } = await axios.post(invocationURL(port), {
body: JSON.stringify(req.body),
})
res.status(data.statusCode)
res.send(data.body)
} catch (e) {
res.status(500)
res.send()
}
},
]
app.post(...makeRequest('/openscad/preview', 5052))
app.post(...makeRequest('/openscad/stl', 5053))
app.post(...makeRequest('/cadquery/stl', 5060))
app.post('/openscad/preview', async (req, res) => {
try {
const { data } = await axios.post(invocationURL(5052), {
body: Buffer.from(JSON.stringify(req.body)).toString('base64'),
})
res.status(data.statusCode)
res.send(data.body)
} catch (e) {
res.status(500)
res.send()
}
})
app.post('/cadquery/stl', async (req, res) => {
console.log('making post request to 5060')
try {
const { data } = await axios.post(invocationURL(5060), {
body: Buffer.from(JSON.stringify(req.body)).toString('base64'),
})
res.status(data.statusCode)
res.send(data.body)
} catch (e) {
res.status(500)
res.send()
}
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)

View File

@@ -36,10 +36,7 @@ RUN npm install
# Get the distribution copy of cq-cli
RUN apt-get install -y libglew2.1
RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.2-beta.2/cq-cli-Linux-x86_64.zip
# Comment the entry above out and uncomment the one below to revert to the stable release
# RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.1.0/cq-cli-Linux-x86_64.zip
RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.1.0/cq-cli-Linux-x86_64.zip
RUN unzip cq-cli-Linux-x86_64.zip
RUN chmod +x cq-cli/cq-cli

View File

@@ -1,57 +1,53 @@
const { runCQ } = require('./runCQ')
const middy = require('middy')
const { cors } = require('middy/middlewares')
const AWS = require('aws-sdk')
const tk = require('timekeeper')
const {
makeHash,
checkIfAlreadyExists,
getObjectUrl,
loggerWrap,
storeAssetAndReturnUrl,
} = require('../common/utils')
const s3 = new AWS.S3()
// cors true does not seem to work in serverless.yml, perhaps docker lambdas aren't covered by that config
// special lambda just for responding to options requests
const preflightOptions = (req, _context, callback) => {
const response = {
statusCode: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': '*',
},
}
callback(null, response)
}
const stl = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = req.body
console.log('eventBody', eventBody)
const key = `${makeHash(eventBody)}.stl`
console.log('key', key)
const params = {
Bucket: process.env.BUCKET,
Key: key,
}
const previousAsset = await checkIfAlreadyExists(params, s3)
if (previousAsset.isAlreadyInBucket) {
console.log('already in bucket')
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
console.log(eventBody, 'eventBody')
const { file, settings } = JSON.parse(eventBody)
const { error, result, tempFile } = await runCQ({ file, settings })
if (error) {
const response = {
statusCode: 400,
body: JSON.stringify({ error, tempFile }),
}
callback(null, response)
} else {
console.log(`got result in route: ${result}, file is: ${tempFile}`)
const fs = require('fs')
const image = fs.readFileSync(`/tmp/${tempFile}/output.stl`, {
encoding: 'base64',
})
console.log(image, 'encoded image')
const response = {
statusCode: 200,
body: JSON.stringify({
url: getObjectUrl(params, s3, tk),
consoleMessage: previousAsset.consoleMessage,
imageBase64: image,
result,
tempFile,
}),
}
callback(null, response)
return
}
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath } = await runCQ({ file, settings })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key,
s3,
params,
tk,
})
}
module.exports = {
stl: middy(loggerWrap(stl)).use(cors()),
stl: middy(stl).use(cors()),
preflightOptions,
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,11 +6,9 @@
"author": "Kurt Hutten <kurt@kurthutten.com>",
"license": "",
"dependencies": {
"aws-sdk": "^2.907.0",
"cors": "^2.8.5",
"middy": "^0.36.0",
"nanoid": "^3.1.20",
"timekeeper": "2.2.0"
"nanoid": "^3.1.20"
},
"devDependencies": {
"aws-lambda-ric": "^1.0.0"

View File

@@ -1,26 +1,15 @@
const { makeFile, runCommand } = require('../common/utils')
const { nanoid } = require('nanoid')
module.exports.runCQ = async ({
file,
settings: { deflection = 0.3 } = {},
} = {}) => {
module.exports.runCQ = async ({ file, settings = {} } = {}) => {
const tempFile = await makeFile(file, '.py', nanoid)
const fullPath = `/tmp/${tempFile}/output.stl`
const command = [
`cq-cli/cq-cli`,
`--codec stl`,
`--infile /tmp/${tempFile}/main.py`,
`--outfile ${fullPath}`,
`--outputopts "deflection:${deflection};angularDeflection:${deflection};"`,
`&& gzip ${fullPath}`,
].join(' ')
const command = `cq-cli/cq-cli --codec stl --infile /tmp/${tempFile}/main.py --outfile /tmp/${tempFile}/output.stl`
console.log('command', command)
try {
const consoleMessage = await runCommand(command, 30000)
return { consoleMessage, fullPath }
const result = await runCommand(command, 30000)
return { result, tempFile }
} catch (error) {
return { error, fullPath }
return { error, tempFile }
}
}

View File

@@ -0,0 +1,386 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
dependencies:
object-assign "^4"
vary "^1"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@1.46.0:
version "1.46.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==
mime-types@~2.1.24:
version "2.1.29"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2"
integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==
dependencies:
mime-db "1.46.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
nanoid@^3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
object-assign@^4:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.9.1"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

View File

@@ -1,19 +1,6 @@
const { exec } = require('child_process')
const { promises } = require('fs')
const { writeFile } = promises
const { createHash } = require('crypto')
const CONSOLE_MESSAGE_KEY = 'console-message-b64'
function putConsoleMessageInMetadata(consoleMessage) {
return {
[CONSOLE_MESSAGE_KEY]: Buffer.from(consoleMessage, 'utf-8').toString(
'base64'
),
}
}
function getConsoleMessageFromMetadata(metadata) {
return Buffer.from(metadata[CONSOLE_MESSAGE_KEY], 'base64').toString('utf-8')
}
async function makeFile(file, extension = '.scad', makeHash) {
const tempFile = 'a' + makeHash() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
@@ -48,118 +35,7 @@ async function runCommand(command, timeout = 5000) {
})
}
function makeHash(script) {
return createHash('sha256').update(script).digest('hex')
}
async function checkIfAlreadyExists(params, s3) {
try {
const objectHead = await s3.headObject(params).promise()
const consoleMessage = getConsoleMessageFromMetadata(objectHead.Metadata)
console.log('consoleMessage', consoleMessage)
return { isAlreadyInBucket: true, consoleMessage }
} catch (e) {
console.log("couldn't find it", e)
return { isAlreadyInBucket: false }
}
}
function getObjectUrl(params, s3, tk) {
const getTruncatedTime = () => {
const currentTime = new Date()
const d = new Date(currentTime)
d.setMinutes(Math.floor(d.getMinutes() / 10) * 10)
d.setSeconds(0)
d.setMilliseconds(0)
return d
}
const HALF_HOUR = 1800
return tk.withFreeze(getTruncatedTime(), () =>
s3.getSignedUrl('getObject', {
...params,
Expires: HALF_HOUR,
})
)
}
function loggerWrap(handler) {
return (req, _context, callback) => {
try {
return handler(req, _context, callback)
} catch (e) {
console.log('error in handler', e)
}
}
}
async function storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key,
s3,
params,
tk,
}) {
if (error) {
const response = {
statusCode: 400,
body: JSON.stringify({ error, fullPath }),
}
callback(null, response)
return
} else {
console.log(`got result in route: ${consoleMessage}, file is: ${fullPath}`)
const { readFile } = require('fs/promises')
let buffer
try {
buffer = await readFile(`${fullPath}.gz`)
} catch (e) {
console.log('read file error', e)
const response = {
statusCode: 400,
body: JSON.stringify({ error: consoleMessage, fullPath }),
}
callback(null, response)
return
}
const FiveDays = 432000
const storedRender = await s3
.putObject({
Bucket: process.env.BUCKET,
Key: key,
Body: buffer,
CacheControl: `max-age=${FiveDays}`, // browser caching to stop downloads of the same part
ContentType: 'text/stl',
ContentEncoding: 'gzip',
Metadata: putConsoleMessageInMetadata(consoleMessage),
})
.promise()
console.log('stored object', storedRender)
const url = getObjectUrl(params, s3, tk)
console.log('url', url)
const response = {
statusCode: 200,
body: JSON.stringify({
url,
consoleMessage,
}),
}
callback(null, response)
return
}
}
module.exports = {
runCommand,
makeFile,
makeHash,
checkIfAlreadyExists,
getObjectUrl,
loggerWrap,
storeAssetAndReturnUrl,
}

View File

@@ -1,37 +1,44 @@
services:
# aws-emulator:
# build: .
# networks:
# - awsland
# ports:
# - "5050:8080"
openscad-preview:
openscad-health:
build:
context: ./
dockerfile: ./openscad/Dockerfile
dockerfile: ./openscad/.
image: openscad
command: openscad.health
ports:
- "5051:8080"
openscad-preview:
image: openscad
# build: ./openscad/.
command: openscad.preview
# networks:
# - awsland
ports:
- "5052:8080"
environment:
AWS_SECRET_ACCESS_KEY: "${DEV_AWS_SECRET_ACCESS_KEY}"
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
BUCKET: "${DEV_BUCKET}"
openscad-stl:
image: openscad
# build: ./openscad/.
command: openscad.stl
ports:
- "5053:8080"
environment:
AWS_SECRET_ACCESS_KEY: "${DEV_AWS_SECRET_ACCESS_KEY}"
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
BUCKET: "${DEV_BUCKET}"
cadquery-stl:
build:
context: ./
dockerfile: ./cadquery/Dockerfile
dockerfile: ./cadquery/.
command: cadquery.stl
ports:
- 5060:8080
environment:
AWS_SECRET_ACCESS_KEY: "${DEV_AWS_SECRET_ACCESS_KEY}"
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
BUCKET: "${DEV_BUCKET}"
# networks:
# awsland:
# name: awsland

View File

@@ -6,10 +6,9 @@ ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update -qq
# double check this below, I'm not sure we need inkscape etc
RUN apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb imagemagick unzip inkscape
RUN add-apt-repository ppa:openscad/releases
RUN apt-get update -qq
RUN apt-get install -y -qq openscad
RUN apt-get install -y curl wget
RUN apt-get install -y curl
# install node14, see comment at the to of node14source_setup.sh
ADD common/node14source_setup.sh /nodesource_setup.sh
@@ -38,14 +37,6 @@ WORKDIR /var/task/
COPY openscad/package*.json /var/task/
RUN npm install
# Install OpenSCAD libraries
# It's experimental, so only adding latest Round-Anything for now
RUN echo "OPENSCADPATH=/var/task/openscad" >>/etc/profile && \
wget -P /var/task/openscad/ https://github.com/Irev-Dev/Round-Anything/archive/refs/tags/1.0.4.zip && \
unzip /var/task/openscad/1.0.4
# Add our own theming (based on DeepOcean with a different "background" and "opencsg-face-back")
COPY openscad/cadhubtheme.json /usr/share/openscad/color-schemes/render/
COPY openscad/*.js /var/task/
COPY common/*.js /var/common/
COPY common/entrypoint.sh /entrypoint.sh

View File

@@ -1,19 +0,0 @@
{
"name" : "CadHub",
"index" : 1600,
"show-in-gui" : true,
"colors" : {
"background" : "#1A1A1D",
"axes-color" : "#c1c1c1",
"opencsg-face-front" : "#eeeeee",
"opencsg-face-back" : "#8732F2",
"cgal-face-front" : "#eeeeee",
"cgal-face-back" : "#0babc8",
"cgal-face-2d" : "#9370db",
"cgal-edge-front" : "#0000ff",
"cgal-edge-back" : "#0000ff",
"cgal-edge-2d" : "#ff00ff",
"crosshair" : "#f0f0f0"
}
}

View File

@@ -1,126 +1,95 @@
const { runScad, stlExport } = require('./runScad')
const middy = require('middy')
const { cors } = require('middy/middlewares')
const AWS = require('aws-sdk')
const tk = require('timekeeper')
const {
makeHash,
checkIfAlreadyExists,
getObjectUrl,
loggerWrap,
storeAssetAndReturnUrl,
} = require('../common/utils')
const s3 = new AWS.S3()
const health = async () => {
console.log('Health endpoint')
return {
statusCode: 200,
body: 'ok',
}
}
const openScadStlKey = (eventBody) => {
const { file } = JSON.parse(eventBody)
return `${makeHash(JSON.stringify(file))}.stl`
// cors true does not seem to work in serverless.yml, perhaps docker lambdas aren't covered by that config
// special lambda just for responding to options requests
const preflightOptions = (req, _context, callback) => {
const response = {
statusCode: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': '*',
},
}
callback(null, response)
}
const preview = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = req.body
console.log('eventBody', eventBody)
const key = `${makeHash(eventBody)}.png`
const stlKey = openScadStlKey(eventBody)
console.log('key', key)
const stlParams = {
Bucket: process.env.BUCKET,
Key: stlKey,
}
const params = {
Bucket: process.env.BUCKET,
Key: key,
}
const [previousAssetStl, previousAssetPng] = await Promise.all([
checkIfAlreadyExists(stlParams, s3),
checkIfAlreadyExists(params, s3),
])
const type = previousAssetStl.isAlreadyInBucket ? 'stl' : 'png'
const previousAsset = previousAssetStl.isAlreadyInBucket
? previousAssetStl
: previousAssetPng
if (previousAsset.isAlreadyInBucket) {
console.log('already in bucket')
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
console.log(eventBody, 'eventBody')
const { file, settings } = JSON.parse(eventBody)
const { error, result, tempFile } = await runScad({ file, settings })
if (error) {
const response = {
statusCode: 400,
body: JSON.stringify({ error, tempFile }),
}
callback(null, response)
} else {
console.log(`got result in route: ${result}, file is: ${tempFile}`)
const fs = require('fs')
const image = fs.readFileSync(`/tmp/${tempFile}/output.png`, {
encoding: 'base64',
})
console.log(image, 'encoded image')
const response = {
statusCode: 200,
body: JSON.stringify({
url: getObjectUrl(
{
Bucket: process.env.BUCKET,
Key: previousAssetStl.isAlreadyInBucket ? stlKey : key,
},
s3,
tk
),
consoleMessage:
previousAsset.consoleMessage || previousAssetPng.consoleMessage,
type,
imageBase64: image,
result,
tempFile,
}),
}
callback(null, response)
return
}
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath } = await runScad({ file, settings })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key,
s3,
params,
tk,
})
}
const stl = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = req.body
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
console.log(eventBody, 'eventBody')
const stlKey = openScadStlKey(eventBody)
console.log('key', stlKey)
const params = {
Bucket: process.env.BUCKET,
Key: stlKey,
}
console.log('original params', params)
const previousAsset = await checkIfAlreadyExists(params, s3)
if (previousAsset.isAlreadyInBucket) {
console.log('already in bucket')
const { file } = JSON.parse(eventBody)
const { error, result, tempFile } = await stlExport({ file })
if (error) {
const response = {
statusCode: 200,
body: JSON.stringify({
url: getObjectUrl({ ...params }, s3, tk),
consoleMessage: previousAsset.consoleMessage,
}),
statusCode: 400,
body: { error, tempFile },
}
callback(null, response)
return
} else {
console.log(`got result in route: ${result}, file is: ${tempFile}`)
const fs = require('fs')
const stl = fs.readFileSync(`/tmp/${tempFile}/output.stl`, {
encoding: 'base64',
})
console.log('encoded stl', stl)
const response = {
statusCode: 200,
headers: {
'content-type': 'application/stl',
},
body: stl,
isBase64Encoded: true,
}
console.log('callback fired')
callback(null, response)
}
const { file } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath } = await stlExport({ file })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key: stlKey,
s3,
params,
tk,
})
}
module.exports = {
health: middy(health).use(cors()),
stl: middy(stl).use(cors()),
preview: middy(loggerWrap(preview)).use(cors()),
preview: middy(preview).use(cors()),
preflightOptions,
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,11 +6,9 @@
"author": "Kurt Hutten <kurt@kurthutten.com>",
"license": "",
"dependencies": {
"aws-sdk": "^2.907.0",
"cors": "^2.8.5",
"middy": "^0.36.0",
"nanoid": "^3.1.20",
"timekeeper": "2.2.0"
"nanoid": "^3.1.20"
},
"devDependencies": {
"aws-lambda-ric": "^1.0.0"

View File

@@ -1,12 +1,6 @@
const { makeFile, runCommand } = require('../common/utils')
const { nanoid } = require('nanoid')
const OPENSCAD_COMMON = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad`
/** Removes our generated/hash filename with just "main.scad", so that it's a nice message in the IDE */
const cleanOpenScadError = (error) =>
error.replace(/["|']\/tmp\/.+\/main.scad["|']/g, "'main.scad'")
module.exports.runScad = async ({
file,
settings: {
@@ -22,43 +16,27 @@ module.exports.runScad = async ({
const { x: rx, y: ry, z: rz } = rotation
const { x: px, y: py, z: pz } = position
const cameraArg = `--camera=${px},${py},${pz},${rx},${ry},${rz},${dist}`
const fullPath = `/tmp/${tempFile}/output.png`
const command = [
OPENSCAD_COMMON,
`-o ${fullPath}`,
cameraArg,
`--imgsize=${x},${y}`,
`--colorscheme CadHub`,
`/tmp/${tempFile}/main.scad`,
`&& gzip ${fullPath}`,
].join(' ')
const command = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad -o /tmp/${tempFile}/output.png ${cameraArg} --imgsize=${x},${y} --colorscheme DeepOcean /tmp/${tempFile}/main.scad`
console.log('command', command)
try {
const consoleMessage = await runCommand(command, 15000)
return { consoleMessage, fullPath }
} catch (dirtyError) {
const error = cleanOpenScadError(dirtyError)
return { error }
const result = await runCommand(command, 15000)
return { result, tempFile }
} catch (error) {
return { error, tempFile }
}
}
module.exports.stlExport = async ({ file } = {}) => {
const tempFile = await makeFile(file, '.scad', nanoid)
const fullPath = `/tmp/${tempFile}/output.stl`
const command = [
OPENSCAD_COMMON,
`--export-format=binstl`,
`-o ${fullPath}`,
`/tmp/${tempFile}/main.scad`,
`&& gzip ${fullPath}`,
].join(' ')
try {
// lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
const consoleMessage = await runCommand(command, 60000)
return { consoleMessage, fullPath }
const result = await runCommand(
`openscad -o /tmp/${tempFile}/output.stl /tmp/${tempFile}/main.scad`,
300000 // lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
)
return { result, tempFile }
} catch (error) {
return { error, fullPath }
return { error, tempFile }
}
}

View File

@@ -0,0 +1,386 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
dependencies:
object-assign "^4"
vary "^1"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@1.46.0:
version "1.46.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==
mime-types@~2.1.24:
version "2.1.29"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2"
integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==
dependencies:
mime-db "1.46.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
nanoid@^3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
object-assign@^4:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.9.1"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

File diff suppressed because it is too large Load Diff

View File

@@ -24,30 +24,60 @@ provider:
file: ./cadquery/Dockerfile
apiGateway:
metrics: true
binaryMediaTypes:
# we need to allow binary types to be able to send back images and stls, but it would be better to be more specific
# ie image/png etc. as */* treats everything as binary including the json body as the input the lambdas
# which mean we need to decode the input bode from base64, but the images break with anything other than */* :(
- '*/*'
# you can overwrite defaults here
# stage: dev
# region: us-east-1
# you can add statements to the Lambda function's IAM Role here
iam:
role:
statements:
- Effect: "Allow"
Action:
- "s3:GetObject"
Resource: "arn:aws:s3:::cad-preview-bucket-prod-001/*"
- Effect: "Allow"
Action:
- "s3:PutObject"
Resource: "arn:aws:s3:::cad-preview-bucket-prod-001/*"
# Dev bucket is cad-preview-bucket-dev-001/*"
# iamRoleStatements:
# - Effect: "Allow"
# Action:
# - "s3:ListBucket"
# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] }
# - Effect: "Allow"
# Action:
# - "s3:PutObject"
# Resource:
# Fn::Join:
# - ""
# - - "arn:aws:s3:::"
# - "Ref" : "ServerlessDeploymentBucket"
# - "/*"
# you can define service wide environment variables here
# environment:
# variable1: value1
functions:
# see preflightoptions comment in openscad.js
preflightopenscadpreview:
image:
name: openscadimage
command:
- openscad.preflightOptions
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: openscad/preview
method: options
preflightopenscadstl:
image:
name: openscadimage
command:
- openscad.preflightOptions
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: openscad/stl
method: options
openscadpreview:
image:
name: openscadimage
@@ -59,10 +89,7 @@ functions:
- http:
path: openscad/preview
method: post
cors: true
timeout: 25
environment:
BUCKET: cad-preview-bucket-prod-001
timeout: 15
openscadstl:
image:
name: openscadimage
@@ -74,10 +101,19 @@ functions:
- http:
path: openscad/stl
method: post
cors: true
timeout: 30
environment:
BUCKET: cad-preview-bucket-prod-001
preflightcadquerystl:
image:
name: cadqueryimage
command:
- cadquery.preflightOptions
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: cadquery/stl
method: options
cadquerystl:
image:
name: cadqueryimage
@@ -89,10 +125,7 @@ functions:
- http:
path: cadquery/stl
method: post
cors: true
timeout: 30
environment:
BUCKET: cad-preview-bucket-prod-001
# The following are a few example events you can configure
# NOTE: Please make sure to change your handler code to work with those events
# Check the event documentation for details
@@ -144,23 +177,3 @@ functions:
# NewOutput:
# Description: "Description for the output"
# Value: "Some output value"
resources:
Resources:
GatewayResponseDefault4XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_4XX
RestApiId:
Ref: 'ApiGatewayRestApi'
GatewayResponseDefault5XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_5XX
RestApiId:
Ref: 'ApiGatewayRestApi'

View File

@@ -0,0 +1,75 @@
/* for local development
Install and run smee (point at this function)
```
yarn global add smee-client
smee --url https://smee.io/3zgDJiGO8TW7nvf --path /.netlify/functions/event_handler --port 8910
```
*/
import { createHmac } from 'crypto'
import { App } from '@octokit/app'
import type { Endpoints } from '@octokit/types'
import type { PullRequestEvent } from '@octokit/webhooks-types'
const app = new App({
privateKey: process.env.GITHUB_APP_PRIVATE_KEY,
appId: process.env.GITHUB_APP_ID,
webhooks: {
secret: process.env.GITHUB_APP_SECRET,
},
})
const signRequestBody = (secret: string, body: string): string =>
'sha256=' + createHmac('sha256', secret).update(body, 'utf-8').digest('hex')
const writePullRequestComment = async ({
event,
message,
}: {
event: PullRequestEvent
message: string
}): Promise<
Endpoints['POST /repos/{owner}/{repo}/issues/{issue_number}/comments']['response']
> => {
const octokit = await app.getInstallationOctokit(event.installation.id)
return octokit.request(
'POST /repos/{owner}/{repo}/issues/{issue_number}/comments',
{
owner: event.repository.owner.login,
repo: event.repository.name,
issue_number: event.number,
body: message,
}
)
}
export const handler = async (req: {
body: string
headers: {
'x-hub-signature-256': string
'x-github-event': string
}
}) => {
const theirSignature = req.headers['x-hub-signature-256']
const ourSignature = signRequestBody(process.env.GITHUB_APP_SECRET, req.body)
if (theirSignature !== ourSignature) {
return {
statusCode: 401,
body: 'Bad signature',
}
}
const eventType = req.headers['x-github-event']
if (eventType !== 'pull_request') {
return { statusCode: 200 }
}
const event: PullRequestEvent = JSON.parse(req.body)
if (['reopened', 'opened'].includes(event.action)) {
await writePullRequestComment({
event,
message: 'Salutations, what a fine PR you have here.',
})
}
return {
statusCode: 200,
}
}

View File

@@ -3,7 +3,6 @@ import {
makeMergedSchema,
makeServices,
} from '@redwoodjs/api'
import { createSentryApolloPlugin } from 'src/lib/sentry'
import schemas from 'src/graphql/**/*.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
@@ -17,7 +16,6 @@ export const handler = createGraphQLHandler({
schemas,
services: makeServices({ services }),
}),
plugins: [createSentryApolloPlugin()],
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()

View File

@@ -1,11 +1,8 @@
import { createUserInsecure } from 'src/services/users/users'
import { createUserInsecure } from 'src/services/users/users.js'
import { db } from 'src/lib/db'
import { sentryWrapper } from 'src/lib/sentry'
import { enforceAlphaNumeric, generateUniqueString } from 'src/services/helpers'
import 'graphql-tag'
import { sendMail } from 'src/lib/sendmail'
const unWrappedHandler = async (req, _context) => {
export const handler = async (req, _context) => {
const body = JSON.parse(req.body)
console.log(body)
console.log(_context)
@@ -57,7 +54,7 @@ const unWrappedHandler = async (req, _context) => {
const user = body.user
const email = user.email
const roles = []
let roles = []
if (eventType === 'signup') {
roles.push('user')
@@ -67,53 +64,13 @@ const unWrappedHandler = async (req, _context) => {
})
const userNameSeed = enforceAlphaNumeric(user?.user_metadata?.userName)
const userName = await generateUniqueString(userNameSeed, isUniqueCallback) // TODO maybe come up with a better default userName?
const name = user?.user_metadata?.full_name
const input = {
email,
userName,
name,
name: user?.user_metadata?.full_name,
id: user.id,
}
await createUserInsecure({ input })
const kurtNotification = sendMail({
to: 'k.hutten@protonmail.ch',
from: {
address: 'news@mail.cadhub.xyz',
name: 'CadHub',
},
subject: `New Cadhub User`,
text: JSON.stringify(input, null, 2),
})
const welcomeMsg = sendMail({
to: email,
from: {
address: 'news@mail.cadhub.xyz',
name: 'CadHub',
},
subject: `${name} - Some things you should know about CadHub`,
text: `Hi, My name's Kurt.
I started CadHub because I wanted a community hub for people who like CodeCAD as much of I do, you should know that the development of CadHub is very much a community effort as well and if you want get involved the discord is the best place to start https://discord.gg/SD7zFRNjGH.
Long term I hope that CadHub will help push CodeCad as a paradigm forward, as there are clear benefits such as: CI/CD for parts, GIT based workflow and CodeCAD parts are normally much more robust to changes to parametric variables because the author can add logic to accommodate big changes where as GUI-CAD usually relies on blackbox heuristics and is more brittle. Sorry I'm getting into the weeds, if you want to read more on the paradigm see our blog https://learn.cadhub.xyz/.
One very easy way to help out is to simply add any OpenSCAD or CadQuery models you have to the website, building out the library of parts atm is very important.
Hit me up anytime for questions or concerns.
Cheers,
Kurt.
k.hutten@protonmail.ch
https://twitter.com/IrevDev
irevdev#1888 - discord
`,
})
try {
await Promise.all([kurtNotification, welcomeMsg])
} catch (e) {
console.log('Problem sending emails', e)
}
return {
statusCode: 200,
@@ -125,5 +82,3 @@ irevdev#1888 - discord
}
}
}
export const handler = sentryWrapper(unWrappedHandler)

View File

@@ -1,39 +0,0 @@
export const schema = gql`
type ProjectReaction {
id: String!
emote: String!
user: User!
userId: String!
project: Project!
projectId: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type Query {
projectReactions: [ProjectReaction!]!
projectReaction(id: String!): ProjectReaction
projectReactionsByProjectId(projectId: String!): [ProjectReaction!]!
}
input ToggleProjectReactionInput {
emote: String!
userId: String!
projectId: String!
}
input UpdateProjectReactionInput {
emote: String
userId: String
projectId: String
}
type Mutation {
toggleProjectReaction(input: ToggleProjectReactionInput!): ProjectReaction!
updateProjectReaction(
id: String!
input: UpdateProjectReactionInput!
): ProjectReaction!
deleteProjectReaction(id: String!): ProjectReaction!
}
`

View File

@@ -4,8 +4,8 @@ export const schema = gql`
text: String!
user: User!
userId: String!
project: Project!
projectId: String!
part: Part!
partId: String!
createdAt: DateTime!
updatedAt: DateTime!
}
@@ -18,13 +18,13 @@ export const schema = gql`
input CreateCommentInput {
text: String!
userId: String!
projectId: String!
partId: String!
}
input UpdateCommentInput {
text: String
userId: String
projectId: String
partId: String
}
type Mutation {

View File

@@ -1,20 +0,0 @@
export const schema = gql`
type Envelope {
from: String
to: [String!]!
}
type EmailResponse {
accepted: [String!]!
rejected: [String!]!
}
input Email {
subject: String!
body: String!
}
type Mutation {
sendAllUsersEmail(input: Email!): EmailResponse!
}
`

View File

@@ -0,0 +1,39 @@
export const schema = gql`
type PartReaction {
id: String!
emote: String!
user: User!
userId: String!
part: Part!
partId: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type Query {
partReactions: [PartReaction!]!
partReaction(id: String!): PartReaction
partReactionsByPartId(partId: String!): [PartReaction!]!
}
input TogglePartReactionInput {
emote: String!
userId: String!
partId: String!
}
input UpdatePartReactionInput {
emote: String
userId: String
partId: String
}
type Mutation {
togglePartReaction(input: TogglePartReactionInput!): PartReaction!
updatePartReaction(
id: String!
input: UpdatePartReactionInput!
): PartReaction!
deletePartReaction(id: String!): PartReaction!
}
`

View File

@@ -0,0 +1,45 @@
export const schema = gql`
type Part {
id: String!
title: String!
description: String
code: String
mainImage: String
createdAt: DateTime!
updatedAt: DateTime!
deleted: Boolean!
user: User!
userId: String!
Comment: [Comment]!
Reaction(userId: String): [PartReaction]!
}
type Query {
parts(userName: String): [Part!]!
part(id: String!): Part
partByUserAndTitle(userName: String!, partTitle: String!): Part
}
input CreatePartInput {
title: String!
description: String
code: String
mainImage: String
userId: String!
}
input UpdatePartInput {
title: String
description: String
code: String
mainImage: String
userId: String
}
type Mutation {
createPart(input: CreatePartInput!): Part!
forkPart(input: CreatePartInput!): Part!
updatePart(id: String!, input: UpdatePartInput!): Part!
deletePart(id: String!): Part!
}
`

View File

@@ -1,58 +0,0 @@
export const schema = gql`
type Project {
id: String!
title: String!
description: String
code: String
mainImage: String
createdAt: DateTime!
updatedAt: DateTime!
user: User!
userId: String!
deleted: Boolean!
cadPackage: CadPackage!
socialCard: SocialCard
Comment: [Comment]!
Reaction(userId: String): [ProjectReaction]!
}
enum CadPackage {
openscad
cadquery
}
type Query {
projects(userName: String): [Project!]!
project(id: String!): Project
projectByUserAndTitle(userName: String!, projectTitle: String!): Project
}
input CreateProjectInput {
title: String
description: String
code: String
mainImage: String
userId: String!
cadPackage: CadPackage!
}
input UpdateProjectInput {
title: String
description: String
code: String
mainImage: String
userId: String
}
type Mutation {
createProject(input: CreateProjectInput!): Project!
forkProject(input: CreateProjectInput!): Project!
updateProject(id: String!, input: UpdateProjectInput!): Project!
updateProjectImages(
id: String!
mainImage64: String
socialCard64: String
): Project!
deleteProject(id: String!): Project!
}
`

View File

@@ -1,16 +0,0 @@
export const schema = gql`
type SocialCard {
id: String!
projectId: String!
project: Project!
createdAt: DateTime!
updatedAt: DateTime!
url: String
outOfDate: Boolean!
}
type Query {
socialCards: [SocialCard!]!
socialCard(id: String!): SocialCard
}
`

View File

@@ -8,9 +8,9 @@ export const schema = gql`
updatedAt: DateTime!
image: String
bio: String
Projects: [Project]!
Project(projectTitle: String): Project
Reaction: [ProjectReaction]!
Parts: [Part]!
Part(partTitle: String): Part
Reaction: [PartReaction]!
Comment: [Comment]!
SubjectAccessRequest: [SubjectAccessRequest]!
}

View File

@@ -121,8 +121,7 @@ export const getCurrentUser = async (decoded, { _token, _type }) => {
* requireAuth({ role: ['editor', 'author'] })
* requireAuth({ role: ['publisher'] })
*/
export const requireAuth = ({ role }: { role?: string | string[] } = {}) => {
console.log(context.currentUser)
export const requireAuth = ({ role } = {}) => {
if (!context.currentUser) {
throw new AuthenticationError("You don't have permission to do that.")
}

View File

@@ -1,45 +0,0 @@
import {
v2 as cloudinary,
UploadApiResponse,
UpdateApiOptions,
} from 'cloudinary'
cloudinary.config({
cloud_name: 'irevdev',
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
})
interface UploadImageArgs {
image64: string
uploadPreset?: string
publicId?: string
invalidate: boolean
}
export const uploadImage = async ({
image64,
uploadPreset = 'CadHub_project_images',
publicId,
invalidate = true,
}: UploadImageArgs): Promise<UploadApiResponse> => {
const options: UpdateApiOptions = { upload_preset: uploadPreset, invalidate }
if (publicId) {
options.public_id = publicId
}
return new Promise((resolve, reject) => {
cloudinary.uploader.upload(image64, options, (error, result) => {
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
export const makeSocialPublicIdServer = (
userName: string,
projectTitle: string
): string => `u-${userName}-slash-p-${projectTitle}`

44
app/api/src/lib/owner.js Normal file
View File

@@ -0,0 +1,44 @@
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
import { db } from 'src/lib/db'
export const requireOwnership = async ({ userId, userName, partId } = {}) => {
// IMPORTANT, don't forget to await this function, as it will only block
// unwanted db actions if it has time to look up resources in the db.
if (!context.currentUser) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (!userId && !userName && !partId) {
throw new ForbiddenError("You don't have access to do that.")
}
if (context.currentUser.roles?.includes('admin')) {
return
}
const netlifyUserId = context.currentUser?.sub
if (userId && userId !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
if (userName) {
const user = await db.user.findUnique({
where: { userName },
})
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
if (partId) {
const user = await db.part
.findUnique({
where: { id: partId },
})
.user()
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
}

View File

@@ -1,94 +0,0 @@
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
import type { Project } from '@prisma/client'
import { db } from 'src/lib/db'
export const requireOwnership = async ({
userId,
userName,
projectId,
sub,
}: {
userId?: string
userName?: string
projectId?: string
sub?: string
} = {}) => {
// IMPORTANT, don't forget to await this function, as it will only block
// unwanted db actions if it has time to look up resources in the db.
if (!(context?.currentUser || sub)) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (!userId && !userName && !projectId) {
throw new ForbiddenError("You don't have access to do that.")
}
if (context.currentUser.roles?.includes('admin')) {
if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
throw new ForbiddenError("That's a local admin ONLY.")
}
return
}
const netlifyUserId = context?.currentUser?.sub || sub
if (userId && userId !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
if (userName) {
const user = await db.user.findUnique({
where: { userName },
})
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
if (projectId) {
const user = await db.project
.findUnique({
where: { id: projectId },
})
.user()
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
}
export const requireProjectOwnership = async ({
projectId,
}: {
userId?: string
userName?: string
projectId?: string
sub?: string
} = {}): Promise<Project> => {
// IMPORTANT, don't forget to await this function, as it will only block
// unwanted db actions if it has time to look up resources in the db.
if (!context?.currentUser) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (!projectId) {
throw new ForbiddenError("You don't have access to do that.")
}
const netlifyUserId = context?.currentUser?.sub
if (projectId || context.currentUser.roles?.includes('admin')) {
if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
throw new ForbiddenError("That's a local admin ONLY.")
}
const project = await db.project.findUnique({
where: { id: projectId },
})
const hasPermission =
(project && project?.userId === netlifyUserId) ||
context.currentUser.roles?.includes('admin')
if (!hasPermission) {
throw new ForbiddenError("You don't own this resource.")
}
return project
}
}

View File

@@ -1,63 +0,0 @@
import nodemailer, { SendMailOptions } from 'nodemailer'
export interface SendMailArgs {
to: string
from: SendMailOptions['from']
subject: string
text: string
}
interface SuccessResult {
accepted: string[]
rejected: string[]
envelopeTime: number
messageTime: number
messageSize: number
response: string
envelope: {
from: string | false
to: string[]
}
messageId: string
}
export function sendMail({
to,
from,
subject,
text,
}: SendMailArgs): Promise<SuccessResult> {
const transporter = nodemailer.createTransport({
host: 'smtp.mailgun.org',
port: 587,
secure: false,
tls: {
ciphers: 'SSLv3',
},
auth: {
user: 'postmaster@mail.cadhub.xyz',
pass: process.env.EMAIL_PASSWORD,
},
})
console.log({ to, from, subject, text })
const emailPromise = new Promise((resolve, reject) => {
transporter.sendMail(
{
from,
to,
subject,
text,
},
(error, info) => {
if (error) {
reject(error)
} else {
resolve(info)
}
}
)
}) as any as Promise<SuccessResult>
return emailPromise
}

View File

@@ -1,105 +0,0 @@
import * as Sentry from '@sentry/node'
import { context, Config, ApolloError } from '@redwoodjs/api'
let sentryInitialized = false
if (process.env.SENTRY_DSN && !sentryInitialized) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.CONTEXT,
release: process.env.COMMIT_REF,
})
sentryInitialized = true
}
async function reportError(error) {
if (!sentryInitialized) return
// If you do have authentication set up, we can add
// some user data to help debug issues
// if (context.currentUser) {
// Sentry.configureScope((scope) => {
// scope.setUser({
// id: context?.currentUser?.id,
// email: context?.currentUser?.email,
// })
// })
// }
if (typeof error === 'string') {
Sentry.captureMessage(error)
} else {
Sentry.captureException(error)
}
await Sentry.flush()
}
export const sentryWrapper = (handler) => async (event, lambdaContext) => {
lambdaContext.callbackWaitsForEmptyEventLoop = false
try {
return await new Promise((resolve, reject) => {
const callback = (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
}
const resp = handler(event, lambdaContext, callback)
if (resp?.then) {
resp.then(resolve, reject)
}
})
} catch (e) {
// This catches both sync errors & promise
// rejections, because we 'await' on the handler
await reportError(e)
throw e
}
}
export const createSentryApolloPlugin: Config['plugins'][number] = () => ({
requestDidStart: () => {
return {
didEncounterErrors(ctx) {
// If we couldn't parse the operation, don't
// do anything here
if (!ctx.operation) {
return
}
for (const err of ctx.errors) {
// Only report internal server errors,
// all errors extending ApolloError should be user-facing
if (err instanceof ApolloError) {
continue
}
// Add scoped report details and send to Sentry
Sentry.withScope((scope) => {
// Annotate whether failing operation was query/mutation/subscription
scope.setTag('kind', ctx.operation.operation)
// Log query and variables as extras (make sure to strip out sensitive data!)
scope.setExtra('query', ctx.request.query)
scope.setExtra('variables', ctx.request.variables)
if (err.path) {
// We can also add the path as breadcrumb
scope.addBreadcrumb({
category: 'query-path',
message: err.path.join(' > '),
level: Sentry.Severity.Debug,
})
}
const transactionId =
ctx.request.http.headers.get('x-transaction-id')
if (transactionId) {
scope.setTransaction(transactionId)
}
Sentry.captureException(err)
})
}
},
}
},
})

View File

@@ -33,6 +33,6 @@ export const deleteComment = ({ id }) => {
export const Comment = {
user: (_obj, { root }) =>
db.comment.findUnique({ where: { id: root.id } }).user(),
project: (_obj, { root }) =>
db.comment.findUnique({ where: { id: root.id } }).project(),
part: (_obj, { root }) =>
db.comment.findUnique({ where: { id: root.id } }).part(),
}

View File

@@ -0,0 +1,9 @@
/*
import { comments } from './comments'
*/
describe('comments', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -1,45 +0,0 @@
import { requireAuth } from 'src/lib/auth'
import { sendMail } from 'src/lib/sendmail'
import type { SendMailArgs } from 'src/lib/sendmail'
import { users } from 'src/services/users/users'
export const sendAllUsersEmail = async ({ input: { body, subject } }) => {
requireAuth({ role: 'admin' })
const from = {
address: 'news@mail.cadhub.xyz',
name: 'CadHub',
}
const emails: SendMailArgs[] = (await users()).map(({ email }) => ({
to: email,
from,
subject,
text: body,
}))
const emailPromises = emails.map((email) => sendMail(email))
const accepted = []
const rejected = []
const result = await Promise.allSettled(emailPromises)
result.forEach((result) => {
if (result.status === 'fulfilled') {
accepted.push(result.value.accepted[0])
} else {
rejected.push(result.reason)
}
})
await sendMail({
to: 'k.hutten@protonmail.ch',
from,
subject: `All users email report`,
text: JSON.stringify(
{
accepted,
rejected,
originalEmailList: emails,
},
null,
2
),
})
return { accepted, rejected }
}

View File

@@ -1,6 +1,4 @@
import { v2 as cloudinary } from 'cloudinary'
import humanId from 'human-id'
cloudinary.config({
cloud_name: 'irevdev',
api_key: process.env.CLOUDINARY_API_KEY,
@@ -22,7 +20,7 @@ export const foreignKeyReplacement = (input) => {
}
export const enforceAlphaNumeric = (string) =>
(string || '').replace(/([^a-zA-Z\d_:])/g, '-')
string.replace(/([^a-zA-Z\d_:])/g, '-')
export const generateUniqueString = async (
seed,
@@ -38,26 +36,6 @@ export const generateUniqueString = async (
return generateUniqueString(newSeed, isUniqueCallback, count)
}
export const generateUniqueStringWithoutSeed = async (
isUniqueCallback: (seed: string) => Promise<any>,
count = 0
) => {
const seed = humanId({
separator: '-',
capitalize: false,
})
const isUnique = !(await isUniqueCallback(seed))
if (isUnique) {
return seed
}
count += 1
if (count > 100) {
console.log('trouble finding unique')
return `very-unique-${seed}`.slice(0, 10)
}
return generateUniqueStringWithoutSeed(isUniqueCallback, count)
}
export const destroyImage = ({ publicId }) =>
new Promise((resolve, reject) => {
cloudinary.uploader.destroy(publicId, (error, result) => {

View File

@@ -5,24 +5,24 @@ import { requireOwnership } from 'src/lib/owner'
import { db } from 'src/lib/db'
import { foreignKeyReplacement } from 'src/services/helpers'
export const projectReactions = () => {
return db.projectReaction.findMany()
export const partReactions = () => {
return db.partReaction.findMany()
}
export const projectReaction = ({ id }) => {
return db.projectReaction.findUnique({
export const partReaction = ({ id }) => {
return db.partReaction.findUnique({
where: { id },
})
}
export const projectReactionsByProjectId = ({ projectId }) => {
return db.projectReaction.findMany({
where: { projectId },
export const partReactionsByPartId = ({ partId }) => {
return db.partReaction.findMany({
where: { partId: partId },
})
}
export const toggleProjectReaction = async ({ input }) => {
// if write fails emote_userId_projectId @@unique constraint, then delete it instead
export const togglePartReaction = async ({ input }) => {
// if write fails emote_userId_partId @@unique constraint, then delete it instead
requireAuth()
await requireOwnership({ userId: input?.userId })
const legalReactions = ['❤️', '👍', '😄', '🙌'] // TODO figure out a way of sharing code between FE and BE, so this is consistent with web/src/components/EmojiReaction/EmojiReaction.js
@@ -36,33 +36,33 @@ export const toggleProjectReaction = async ({ input }) => {
let dbPromise
const inputClone = { ...input } // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now
try {
dbPromise = await db.projectReaction.create({
dbPromise = await db.partReaction.create({
data: foreignKeyReplacement(input),
})
} catch (e) {
dbPromise = db.projectReaction.delete({
where: { emote_userId_projectId: inputClone },
dbPromise = db.partReaction.delete({
where: { emote_userId_partId: inputClone },
})
}
return dbPromise
}
export const updateProjectReaction = ({ id, input }) => {
return db.projectReaction.update({
export const updatePartReaction = ({ id, input }) => {
return db.partReaction.update({
data: foreignKeyReplacement(input),
where: { id },
})
}
export const deleteProjectReaction = ({ id }) => {
return db.projectReaction.delete({
export const deletePartReaction = ({ id }) => {
return db.partReaction.delete({
where: { id },
})
}
export const ProjectReaction = {
export const PartReaction = {
user: (_obj, { root }) =>
db.projectReaction.findUnique({ where: { id: root.id } }).user(),
project: (_obj, { root }) =>
db.projectReaction.findUnique({ where: { id: root.id } }).project(),
db.partReaction.findUnique({ where: { id: root.id } }).user(),
part: (_obj, { root }) =>
db.partReaction.findUnique({ where: { id: root.id } }).part(),
}

View File

@@ -0,0 +1,9 @@
/*
import { partReactions } from './partReactions'
*/
describe('partReactions', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -0,0 +1,113 @@
import { db } from 'src/lib/db'
import {
foreignKeyReplacement,
enforceAlphaNumeric,
generateUniqueString,
destroyImage,
} from 'src/services/helpers'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'
export const parts = ({ userName }) => {
if (!userName) {
return db.part.findMany({ where: { deleted: false } })
}
return db.part.findMany({
where: {
deleted: false,
user: {
userName,
},
},
})
}
export const part = ({ id }) => {
return db.part.findUnique({
where: { id },
})
}
export const partByUserAndTitle = async ({ userName, partTitle }) => {
const user = await db.user.findUnique({
where: {
userName,
},
})
return db.part.findUnique({
where: {
title_userId: {
title: partTitle,
userId: user.id,
},
},
})
}
export const createPart = async ({ input }) => {
requireAuth()
return db.part.create({
data: foreignKeyReplacement(input),
})
}
export const forkPart = async ({ input }) => {
// Only difference between create and fork part is that fork part will generate a unique title
// (for the user) if there is a conflict
const isUniqueCallback = async (seed) =>
db.part.findUnique({
where: {
title_userId: {
title: seed,
userId: input.userId,
},
},
})
const title = await generateUniqueString(input.title, isUniqueCallback)
// TODO change the description to `forked from userName/partName ${rest of description}`
return db.part.create({
data: foreignKeyReplacement({ ...input, title }),
})
}
export const updatePart = async ({ id, input }) => {
requireAuth()
await requireOwnership({ partId: id })
if (input.title) {
input.title = enforceAlphaNumeric(input.title)
}
const originalPart = await db.part.findUnique({ where: { id } })
const imageToDestroy =
originalPart.mainImage !== input.mainImage && originalPart.mainImage
const update = await db.part.update({
data: foreignKeyReplacement(input),
where: { id },
})
if (imageToDestroy) {
console.log(`image destroyed, publicId: ${imageToDestroy}, partId: ${id}`)
// destroy after the db has been updated
destroyImage({ publicId: imageToDestroy })
}
return update
}
export const deletePart = async ({ id }) => {
requireAuth()
await requireOwnership({ partId: id })
return db.part.update({
data: {
deleted: true,
},
where: { id },
})
}
export const Part = {
user: (_obj, { root }) =>
db.part.findUnique({ where: { id: root.id } }).user(),
Comment: (_obj, { root }) =>
db.part.findUnique({ where: { id: root.id } }).Comment(),
Reaction: (_obj, { root }) =>
db.part
.findUnique({ where: { id: root.id } })
.Reaction({ where: { userId: _obj.userId } }),
}

View File

@@ -0,0 +1,9 @@
/*
import { parts } from './parts'
*/
describe('parts', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -1,266 +0,0 @@
import type { Prisma, Project as ProjectType } from '@prisma/client'
import type { ResolverArgs } from '@redwoodjs/api'
import { uploadImage, makeSocialPublicIdServer } from 'src/lib/cloudinary'
import { db } from 'src/lib/db'
import {
foreignKeyReplacement,
enforceAlphaNumeric,
generateUniqueString,
generateUniqueStringWithoutSeed,
destroyImage,
} from 'src/services/helpers'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership, requireProjectOwnership } from 'src/lib/owner'
import { socialCard } from '../socialCards/socialCards'
export const projects = ({ userName }) => {
if (!userName) {
return db.project.findMany({ where: { deleted: false } })
}
return db.project.findMany({
where: {
deleted: false,
user: {
userName,
},
},
})
}
export const project = ({ id }: Prisma.ProjectWhereUniqueInput) => {
return db.project.findUnique({
where: { id },
})
}
export const projectByUserAndTitle = async ({ userName, projectTitle }) => {
const user = await db.user.findUnique({
where: {
userName,
},
})
return db.project.findUnique({
where: {
title_userId: {
title: projectTitle,
userId: user.id,
},
},
})
}
const isUniqueProjectTitle = (userId: string) => async (seed: string) =>
db.project.findUnique({
where: {
title_userId: {
title: seed,
userId,
},
},
})
interface CreateProjectArgs {
input: Prisma.ProjectCreateArgs['data']
}
export const createProject = async ({ input }: CreateProjectArgs) => {
requireAuth()
console.log(input.userId)
const isUniqueCallback = isUniqueProjectTitle(input.userId)
let title = input.title
if (!title) {
title = await generateUniqueStringWithoutSeed(isUniqueCallback)
}
return db.project.create({
data: foreignKeyReplacement({
...input,
title,
}),
})
}
export const forkProject = async ({ input }) => {
// Only difference between create and fork project is that fork project will generate a unique title
// (for the user) if there is a conflict
const isUniqueCallback = isUniqueProjectTitle(input.userId)
const title = await generateUniqueString(input.title, isUniqueCallback)
// TODO change the description to `forked from userName/projectName ${rest of description}`
return db.project.create({
data: foreignKeyReplacement({ ...input, title }),
})
}
interface UpdateProjectArgs extends Prisma.ProjectWhereUniqueInput {
input: Prisma.ProjectUpdateInput
}
export const updateProject = async ({ id, input }: UpdateProjectArgs) => {
const checkSocialCardValidity = async (
projectId: string,
input: UpdateProjectArgs['input'],
oldProject: ProjectType
) => {
const titleChange = input.title && input.title !== oldProject.title
const descriptionChange =
input.description && input.description !== oldProject.description
if (titleChange || descriptionChange) {
const socialCard = await db.socialCard.findUnique({where: {projectId}})
if (socialCard) {
return db.socialCard.update({
data: { outOfDate: true },
where: { id: socialCard.id },
})
}
}
}
requireAuth()
const originalProject = await requireProjectOwnership({ projectId: id })
console.log('yooooo', originalProject)
if (input.title) {
input.title = enforceAlphaNumeric(input.title)
}
const socialCardPromise = checkSocialCardValidity(id, input, originalProject)
const imageToDestroy =
originalProject.mainImage !== input.mainImage &&
input.mainImage &&
originalProject.mainImage
const update = await db.project.update({
data: foreignKeyReplacement(input),
where: { id },
})
if (imageToDestroy) {
console.log(
`image destroyed, publicId: ${imageToDestroy}, projectId: ${id}, replacing image is ${input.mainImage}`
)
// destroy after the db has been updated
await destroyImage({ publicId: imageToDestroy })
}
await socialCardPromise
return update
}
export const updateProjectImages = async ({
id,
mainImage64,
socialCard64,
}: {
id: string
mainImage64?: string
socialCard64?: string
}): Promise<ProjectType> => {
requireAuth()
const project = await requireProjectOwnership({ projectId: id })
const replaceSocialCard = async () => {
if (!socialCard64) {
return
}
let publicId = ''
let socialCardId = ''
try {
;({ id: socialCardId, url: publicId } = await db.socialCard.findUnique({
where: { projectId: id },
}))
} catch (e) {
const { userName } = await db.user.findUnique({
where: { id: project.userId },
})
publicId = makeSocialPublicIdServer(userName, project.title)
}
const imagePromise = uploadImage({
image64: socialCard64,
uploadPreset: 'CadHub_project_images',
publicId,
invalidate: true,
})
const saveOrUpdateSocialCard = () => {
const data = {
outOfDate: false,
url: publicId,
}
if (socialCardId) {
return db.socialCard.update({
data,
where: { projectId: id },
})
}
return db.socialCard.create({
data: {
...data,
project: {
connect: {
id: id,
},
},
},
})
}
const socialCardUpdatePromise = saveOrUpdateSocialCard()
const [socialCard] = await Promise.all([
socialCardUpdatePromise,
imagePromise,
])
return socialCard
}
const updateMainImage = async (): Promise<ProjectType> => {
if (!mainImage64) {
return project
}
const { public_id: mainImage } = await uploadImage({
image64: mainImage64,
uploadPreset: 'CadHub_project_images',
invalidate: true,
})
const projectPromise = db.project.update({
data: {
mainImage,
},
where: { id },
})
let imageDestroyPromise = new Promise((r) => r(null))
if (project.mainImage) {
console.log(
`image destroyed, publicId: ${project.mainImage}, projectId: ${id}, replacing image is ${mainImage}`
)
// destroy after the db has been updated
imageDestroyPromise = destroyImage({ publicId: project.mainImage })
}
const [updatedProject] = await Promise.all([
projectPromise,
imageDestroyPromise,
])
return updatedProject
}
const [updatedProject] = await Promise.all([
updateMainImage(),
replaceSocialCard(),
])
return updatedProject
}
export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
requireAuth()
await requireOwnership({ projectId: id })
return db.project.update({
data: {
deleted: true,
},
where: { id },
})
}
export const Project = {
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project.findUnique({ where: { id: root.id } }).user(),
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project.findUnique({ where: { id: root.id } }).socialCard(),
Comment: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project
.findUnique({ where: { id: root.id } })
.Comment({ orderBy: { createdAt: 'desc' } }),
Reaction: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project
.findUnique({ where: { id: root.id } })
.Reaction({ where: { userId: _obj.userId } }),
}

View File

@@ -1,25 +0,0 @@
import type { Prisma } from '@prisma/client'
import type { ResolverArgs, BeforeResolverSpecType } from '@redwoodjs/api'
import { db } from 'src/lib/db'
import { requireAuth } from 'src/lib/auth'
// Used when the environment variable REDWOOD_SECURE_SERVICES=1
export const beforeResolver = (rules: BeforeResolverSpecType) => {
rules.add(requireAuth)
}
export const socialCards = () => {
return db.socialCard.findMany()
}
export const socialCard = ({ id }: Prisma.SocialCardWhereUniqueInput) => {
return db.socialCard.findUnique({
where: { id },
})
}
export const SocialCard = {
project: (_obj, { root }: ResolverArgs<ReturnType<typeof socialCard>>) =>
db.socialCard.findUnique({ where: { id: root.id } }).project(),
}

View File

@@ -0,0 +1,9 @@
/*
import { subjectAccessRequests } from './subjectAccessRequests'
*/
describe('subjectAccessRequests', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -51,9 +51,9 @@ export const updateUserByUserName = async ({ userName, input }) => {
`You've tried to used a protected word as you userName, try something other than `
)
}
const originalProject = await db.user.findUnique({ where: { userName } })
const originalPart = await db.user.findUnique({ where: { userName } })
const imageToDestroy =
originalProject.image !== input.image && originalProject.image
originalPart.image !== input.image && originalPart.image
const update = await db.user.update({
data: input,
where: { userName },
@@ -73,14 +73,14 @@ export const deleteUser = ({ id }) => {
}
export const User = {
Projects: (_obj, { root }) =>
db.user.findUnique({ where: { id: root.id } }).Project(),
Project: (_obj, { root }) =>
_obj.projectTitle &&
db.project.findUnique({
Parts: (_obj, { root }) =>
db.user.findUnique({ where: { id: root.id } }).Part(),
Part: (_obj, { root }) =>
_obj.partTitle &&
db.part.findUnique({
where: {
title_userId: {
title: _obj.projectTitle,
title: _obj.partTitle,
userId: root.id,
},
},

View File

@@ -0,0 +1,9 @@
/*
import { users } from './users'
*/
describe('users', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"esModuleInterop": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"src/*": ["./src/*"]
},
"typeRoots": ["../node_modules/@types", "./node_modules/@types"],
"types": ["jest"]
},
"include": ["src", "../.redwood/**/*"]
}

View File

@@ -1,5 +1,7 @@
const { getPaths } = require('@redwoodjs/internal')
const { getConfig } = require('@redwoodjs/internal')
const config = getConfig()
module.exports = {
schema: getPaths().generated.schema,
schema: `http://${config.api.host}:${config.api.port}/graphql`,
}

View File

@@ -4,16 +4,7 @@ publish = "web/dist"
functions = "api/dist/functions"
[dev]
# To use [Netlify Dev](https://www.netlify.com/products/dev/),
# install netlify-cli from https://docs.netlify.com/cli/get-started/#installation
# and then use netlify link https://docs.netlify.com/cli/get-started/#link-and-unlink-sites
# to connect your local project to a site already on Netlify
# then run netlify dev and our app will be accessible on the port specified below
framework = "redwoodjs"
# Set targetPort to the [web] side port as defined in redwood.toml
targetPort = 8910
# Point your browser to this port to access your RedwoodJS app
port = 8888
command = "yarn rw dev"
[[redirects]]
from = "/*"
@@ -22,10 +13,3 @@ functions = "api/dist/functions"
[context.deploy-preview.environment]
CAD_LAMBDA_BASE_URL = "https://t7wdlz8ztf.execute-api.us-east-1.amazonaws.com/dev2"
[[plugins]]
package = "@sentry/netlify-build-plugin"
[plugins.inputs]
sentryOrg = "kurt"
sentryProject = "kurt"

View File

@@ -8,25 +8,17 @@
},
"scripts": {},
"devDependencies": {
"@redwoodjs/core": "^0.34.1"
"@redwoodjs/core": "^0.31.0"
},
"eslintConfig": {
"extends": "@redwoodjs/eslint-config",
"rules": {
"react/no-unescaped-entities": [
"error",
{
"forbid": [
">",
"}",
"\""
]
}
]
}
"extends": "@redwoodjs/eslint-config"
},
"engines": {
"node": ">=14",
"yarn": ">=1.15"
},
"resolutions": {
"react": "17.0.1",
"react-dom": "17.0.1"
}
}
}

View File

@@ -8,24 +8,10 @@
[web]
port = 8910
apiProxyPath = "/.netlify/functions"
includeEnvironmentVariables = [
'GOOGLE_ANALYTICS_ID',
'CLOUDINARY_API_KEY',
'CLOUDINARY_API_SECRET',
'CAD_LAMBDA_BASE_URL',
'SENTRY_DSN',
'SENTRY_AUTH_TOKEN',
'SENTRY_ORG',
'SENTRY_PROJECT',
'EMAIL_PASSWORD'
]
includeEnvironmentVariables = ['GOOGLE_ANALYTICS_ID', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET', 'CAD_LAMBDA_BASE_URL']
# experimentalFastRefresh = true # this seems to break cascadeStudio
[api]
port = 8911
schemaPath = "./api/db/schema.prisma"
[browser]
open = true
[experimental]
esbuild = true

View File

@@ -1,9 +1,92 @@
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
module.exports = (config, { env }) => {
config.plugins.forEach((plugin) => {
if (plugin.constructor.name === 'HtmlWebpackPlugin') {
plugin.options.favicon = './src/favicon.svg'
} else if (plugin.constructor.name === 'CopyPlugin') {
plugin.patterns.push({
from: './src/cascade/js/StandardLibraryIntellisense.ts',
to: 'js/StandardLibraryIntellisense.ts',
})
plugin.patterns.push({
from: './src/cascade/static_node_modules/opencascade.js/dist/oc.d.ts',
to: 'opencascade.d.ts',
})
plugin.patterns.push({
from: '../node_modules/three/src/Three.d.ts',
to: 'Three.d.ts',
})
plugin.patterns.push({
from: './src/cascade/fonts',
to: 'fonts',
})
plugin.patterns.push({
from: './src/cascade/textures',
to: 'textures',
})
}
})
config.plugins.push(
new MonacoWebpackPlugin({
languages: ['typescript'],
features: [
'accessibilityHelp',
'anchorSelect',
'bracketMatching',
'caretOperations',
'clipboard',
'codeAction',
'codelens',
'comment',
'contextmenu',
'coreCommands',
'cursorUndo',
'documentSymbols',
'find',
'folding',
'fontZoom',
'format',
'gotoError',
'gotoLine',
'gotoSymbol',
'hover',
'inPlaceReplace',
'indentation',
'inlineHints',
'inspectTokens',
'linesOperations',
'linkedEditing',
'links',
'multicursor',
'parameterHints',
'quickCommand',
'quickHelp',
'quickOutline',
'referenceSearch',
'rename',
'smartSelect',
'snippets',
'suggest',
'toggleHighContrast',
'toggleTabFocusMode',
'transpose',
'unusualLineTerminators',
'viewportSemanticTokens',
'wordHighlighter',
'wordOperations',
'wordPartOperations',
],
})
)
config.module.rules[0].oneOf.push({
test: /opencascade\.wasm\.wasm$/,
type: 'javascript/auto',
loader: 'file-loader',
})
config.node = {
fs: 'empty',
}
return config
}

View File

@@ -1 +1,6 @@
module.exports = require('@redwoodjs/testing/config/jest/api')
const { getConfig } = require('@redwoodjs/core')
const config = getConfig({ type: 'jest', target: 'browser' })
config.displayName.name = 'web'
module.exports = config

View File

@@ -16,41 +16,40 @@
"@headlessui/react": "^1.0.0",
"@material-ui/core": "^4.11.0",
"@monaco-editor/react": "^4.0.11",
"@react-three/drei": "^7.3.1",
"@react-three/fiber": "^7.0.5",
"@redwoodjs/auth": "^0.34.1",
"@redwoodjs/forms": "^0.34.1",
"@redwoodjs/router": "^0.34.1",
"@redwoodjs/web": "^0.34.1",
"@sentry/browser": "^6.5.1",
"@tailwindcss/aspect-ratio": "0.2.1",
"browser-fs-access": "^0.17.2",
"@redwoodjs/auth": "^0.31.0",
"@redwoodjs/forms": "^0.31.0",
"@redwoodjs/router": "^0.31.0",
"@redwoodjs/web": "^0.31.0",
"cloudinary-react": "^1.6.7",
"controlkit": "^0.1.9",
"get-active-classes": "^0.0.11",
"golden-layout": "^1.5.9",
"gotrue-js": "^0.9.27",
"html-to-image": "^1.7.0",
"lodash": "^4.17.21",
"jquery": "^3.5.1",
"monaco-editor": "^0.20.0",
"monaco-editor-webpack-plugin": "^1.9.1",
"netlify-identity-widget": "^1.9.1",
"pako": "^2.0.3",
"opencascade.js": "^0.1.15",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-dropzone": "^11.2.1",
"react-ga": "^3.3.0",
"react-helmet": "^6.1.0",
"react-image-crop": "^8.6.6",
"react-mosaic-component": "^4.1.1",
"react-tabs": "^3.2.2",
"react-three-fiber": "^5.3.19",
"rich-markdown-editor": "^11.0.2",
"styled-components": "^5.2.0",
"three": "^0.130.1"
"three": "^0.118.3"
},
"devDependencies": {
"@types/lodash": "^4.14.170",
"postcss": "^8.2.13",
"autoprefixer": "^10.2.5",
"html-webpack-plugin": "^4.5.0",
"postcss": "^8.2.13",
"opentype.js": "^1.3.3",
"postcss-loader": "4.0.2",
"tailwindcss": "^2.1.2"
"tailwindcss": "^2.1.2",
"worker-loader": "^3.0.7"
}
}

View File

@@ -1,623 +0,0 @@
(function(f) {
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = f()
} else if (typeof define === "function" && define.amd) {
define([], f)
} else {
var g;
if (typeof window !== "undefined") {
g = window
} else if (typeof global !== "undefined") {
g = global
} else if (typeof self !== "undefined") {
g = self
} else {
g = this
}
g.jscadWorker = f()
}
})(function() {
// multi purpose module
const setPoints = (points, p, i)=>{
points[i++] = p[0]
points[i++] = p[1]
points[i++] = p[2] || 0
}
function CSG2Vertices(csg){
let idx = 0
let vLen = 0, iLen = 0
for (let poly of csg.polygons){
let len = poly.vertices.length
vLen += len *3
iLen += 3 * (len-2)
}
const vertices = new Float32Array(vLen)
const indices = vLen > 65535 ? new Uint32Array(iLen) : new Uint16Array(iLen)
let vertOffset = 0
let indOffset = 0
let posOffset = 0
let first = 0
for (let poly of csg.polygons){
let arr = poly.vertices
let len = arr.length
first = posOffset
vertices.set(arr[0], vertOffset)
vertOffset +=3
vertices.set(arr[1], vertOffset)
vertOffset +=3
posOffset +=2
for(let i=2; i<len; i++){
vertices.set(arr[i], vertOffset)
indices[indOffset++] = first
indices[indOffset++] = first + i -1
indices[indOffset++] = first + i
vertOffset += 3
posOffset += 1
}
}
return {vertices, indices, type:'mesh'}
}
function CSG2LineVertices(csg){
let vLen = csg.points.length * 3
if(csg.isClosed) vLen += 3
var vertices = new Float32Array(vLen)
csg.points.forEach((p,idx)=>setPoints(vertices, p, idx * 3 ))
if(csg.isClosed){
setPoints(vertices, csg.points[0], vertices.length - 3 )
}
return {vertices, type:'line'}
}
function CSG2LineSegmentsVertices(csg){
let vLen = csg.sides.length * 6
var vertices = new Float32Array(vLen)
csg.sides.forEach((side,idx)=>{
let i = idx * 6
setPoints(vertices, side[0], i)
setPoints(vertices, side[1], i+3)
})
return {vertices, type:'lines'}
}
function CSGCached(func, data, cacheKey, transferable){
cacheKey = cacheKey || data
let geo = CSGToBuffers.cache.get(cacheKey)
if(geo) return geo
geo = func(data)
// fill transferable array for postMessage optimization
if(transferable){
const {vertices, indices} = geo
transferable.push(vertices)
if(indices) transferable.push(indices)
}
CSGToBuffers.cache.set(cacheKey, geo)
return geo
}
function CSGToBuffers(csg, transferable){
let obj
if(csg.polygons) obj = CSGCached(CSG2Vertices,csg,csg.polygons, transferable)
if(csg.sides && !csg.points) obj = CSGCached(CSG2LineSegmentsVertices,csg,csg.sides, transferable)
if(csg.points) obj = CSGCached(CSG2LineVertices,csg,csg.points, transferable)
return obj
}
CSGToBuffers.clearCache = ()=>{CSGToBuffers.cache = new WeakMap()}
CSGToBuffers.clearCache()
let workerBaseURI
function require(url){
url = require.alias[url] || url
if(url[0] != '/' && url.substr(0,2) != './' && url.substr(0,4) != 'http') url = 'https://unpkg.com/'+url
let exports=require.cache[url]; //get from cache
if (!exports) { //not cached
let module = requireModule(url)
require.cache[url] = exports = module.exports; //cache obj exported by module
}
return exports; //require returns object exported by module
}
function requireFile(url){
try{
let X=new XMLHttpRequest();
X.open("GET", new URL(url,workerBaseURI), 0); // sync
X.send();
if (X.status && X.status !== 200) throw new Error(X.statusText);
return X.responseText;
}catch(e){
console.log('problem loading url ',url,'base',workerBaseURI,' error:',e.message)
throw e
}
}
function requireModule(url, source){
try {
const exports={};
if(!source) source = requireFile(url)
const module = { id: url, uri: url, exports:exports, source }; //according to node.js modules
// fix, add comment to show source on Chrome Dev Tools
source="//@ sourceURL="+url+"\n" + source;
//------
const anonFn = new Function("require", "exports", "module", source); //create a Fn with module code, and 3 params: require, exports & module
anonFn(require, exports, module); // call the Fn, Execute the module
return module
} catch (err) {
console.error("Error loading module "+url, err.message);
throw err;
}
}
require.cache = {}
require.alias = {}
const initCanvas = (canvas, callback)=>{
// convert HTML events (mouse movement) to viewer changes
let lastX = 0
let lastY = 0
let pointerDown = false
const moveHandler = (ev) => {
if(!pointerDown) return
const cmd = {
worker: 'render',
dx: lastX - ev.pageX,
dy: ev.pageY - lastY
}
const shiftKey = (ev.shiftKey === true) || (ev.touches && ev.touches.length > 2)
cmd.action = shiftKey ? 'pan':'rotate'
callback(cmd)
lastX = ev.pageX
lastY = ev.pageY
ev.preventDefault()
}
const downHandler = (ev) => {
pointerDown = true
lastX = ev.pageX
lastY = ev.pageY
canvas.setPointerCapture(ev.pointerId)
ev.preventDefault()
}
const upHandler = (ev) => {
pointerDown = false
canvas.releasePointerCapture(ev.pointerId)
ev.preventDefault()
}
const wheelHandler = (ev) => {
callback({action:'zoom', dy:ev.deltaY, worker: 'render'})
ev.preventDefault()
}
canvas.onpointermove = moveHandler
canvas.onpointerdown = downHandler
canvas.onpointerup = upHandler
canvas.onwheel = wheelHandler
}
const cmdHandler = (handlers)=>(cmd)=>{
const fn = handlers[cmd.action]
if (!fn) throw new Error('no handler for type: ' + cmd.action)
fn(cmd);
}
const makeScriptWorker = ({callback, convertToSolids})=>{
let workerBaseURI, onInit
function runMain(params={}){
let time = Date.now()
let solids
let transfer = []
try{
solids = main(params)
}catch(e){
callback({action:'entities', worker:'render', error:e.message, stack:e.stack.toString()}, transfer)
return
}
let solidsTime = Date.now() - time
scriptStats = `generate solids ${solidsTime}ms`
if(convertToSolids === 'buffers'){
CSGToBuffers.clearCache()
entities = solids.map((csg)=>{
let obj = CSGToBuffers(csg, transfer)
obj.color = csg.color
obj.transforms = csg.transforms
return obj
})
}else if(convertToSolids === 'regl'){
const { entitiesFromSolids } = require('@jscad/regl-renderer')
time = Date.now()
entities = entitiesFromSolids({}, solids)
scriptStats += ` convert to entities ${Date.now()-time}ms`
}else{
entities = solids
}
callback({action:'entities', worker:'render', entities, scriptStats}, transfer)
}
let initialized = false
const handlers = {
runScript: ({script,url, params={}})=>{
if(!initialized){
onInit = ()=>handlers.runScript({script,url, params})
}
let script_module
try{
script_module = requireModule(url,script)
}catch(e){
callback({action:'entities', worker:'render', error:e.message, stack:e.stack.toString()})
return
}
main = script_module.exports.main
let gp = script_module.exports.getParameterDefinitions
if(gp){
callback({action:'parameterDefinitions', worker:'main', data:gp()})
}
runMain(params)
},
updateParams: ({params={}})=>{
runMain(params)
},
init: (params)=>{
let {baseURI, alias=[]} = params
if(!baseURI && typeof document != 'undefined' && document.baseURI){
baseURI = document.baseURI
}
if(baseURI) workerBaseURI = baseURI.toString()
alias.forEach(arr=>{
let [orig, ...aliases] = arr
aliases.forEach(a=>{
require.alias[a] = orig
if(a.toLowerCase().substr(-3)!=='.js') require.alias[a+'.js'] = orig
})
})
initialized = true
if(onInit) onInit()
},
}
return {
// called from outside to pass mesasges into worker
postMessage: cmdHandler(handlers),
}
}
/** Make render worker */
const makeRenderWorker = ()=>{
let perspectiveCamera
const state = {}
const rotateSpeed = 0.002
const panSpeed = 1
const zoomSpeed = 0.08
let rotateDelta = [0, 0]
let panDelta = [0, 0]
let zoomDelta = 0
let updateRender = true
let orbitControls, renderOptions, gridOptions, axisOptions, renderer
let entities = []
function createContext (canvas, contextAttributes) {
function get (type) {
try {
return {gl:canvas.getContext(type, contextAttributes), type}
} catch (e) {
return null
}
}
return (
get('webgl2') ||
get('webgl') ||
get('experimental-webgl') ||
get('webgl-experimental')
)
}
const startRenderer = ({canvas, cameraPosition, cameraTarget, axis={}, grid={}})=>{
const { prepareRender, drawCommands, cameras, controls } = require('@jscad/regl-renderer')
perspectiveCamera = cameras.perspective
orbitControls = controls.orbit
state.canvas = canvas
state.camera = Object.assign({}, perspectiveCamera.defaults)
if(cameraPosition) state.camera.position = cameraPosition
if(cameraTarget) state.camera.target = cameraTarget
resize({ width:canvas.width, height:canvas.height })
state.controls = orbitControls.defaults
const {gl, type} = createContext(canvas)
// prepare the renderer
const setupOptions = {
glOptions: {gl}
}
if(type == 'webgl'){
setupOptions.glOptions.optionalExtensions = ['oes_element_index_uint']
}
renderer = prepareRender(setupOptions)
gridOptions = {
visuals: {
drawCmd: 'drawGrid',
show: grid.show || grid.show === undefined ,
color: grid.color || [0, 0, 0, 1],
subColor: grid.subColor || [0, 0, 1, 0.5],
fadeOut: false,
transparent: true
},
size: grid.size || [200, 200],
ticks: grid.ticks || [10, 1]
}
axisOptions = {
visuals: {
drawCmd: 'drawAxis',
show: axis.show || axis.show === undefined
},
size: axis.size || 100,
}
// assemble the options for rendering
renderOptions = {
camera: state.camera,
drawCommands: {
drawAxis: drawCommands.drawAxis,
drawGrid: drawCommands.drawGrid,
drawLines: drawCommands.drawLines,
drawMesh: drawCommands.drawMesh
},
// define the visual content
entities: [
gridOptions,
axisOptions,
...entities
]
}
// the heart of rendering, as themes, controls, etc change
updateView()
}
let renderTimer
const tmFunc = typeof requestAnimationFrame === 'undefined' ? setTimeout : requestAnimationFrame
function updateView(delay=8){
if(renderTimer || !renderer) return
renderTimer = tmFunc(updateAndRender,delay)
}
const doRotatePanZoom = () => {
if (rotateDelta[0] || rotateDelta[1]) {
const updated = orbitControls.rotate({ controls: state.controls, camera: state.camera, speed: rotateSpeed }, rotateDelta)
state.controls = { ...state.controls, ...updated.controls }
rotateDelta = [0, 0]
}
if (panDelta[0] || panDelta[1]) {
const updated = orbitControls.pan({ controls:state.controls, camera:state.camera, speed: panSpeed }, panDelta)
state.controls = { ...state.controls, ...updated.controls }
panDelta = [0, 0]
state.camera.position = updated.camera.position
state.camera.target = updated.camera.target
}
if (zoomDelta) {
const updated = orbitControls.zoom({ controls:state.controls, camera:state.camera, speed: zoomSpeed }, zoomDelta)
state.controls = { ...state.controls, ...updated.controls }
zoomDelta = 0
}
}
const updateAndRender = (timestamp) => {
renderTimer = null
doRotatePanZoom()
const updates = orbitControls.update({ controls: state.controls, camera: state.camera })
state.controls = { ...state.controls, ...updates.controls }
if(state.controls.changed) updateView(16) // for elasticity in rotate / zoom
state.camera.position = updates.camera.position
perspectiveCamera.update(state.camera)
renderOptions.entities = [
gridOptions,
axisOptions,
...entities
]
let time = Date.now()
renderer(renderOptions)
if(updateRender){
updateRender = '';
}
}
function resize({width,height}){
state.canvas.width = width
state.canvas.height = height
perspectiveCamera.setProjection(state.camera, state.camera, { width, height })
perspectiveCamera.update(state.camera, state.camera)
updateView()
}
const handlers = {
pan: ({dx,dy})=>{
panDelta[0] += dx
panDelta[1] += dy
updateView()
},
rotate: ({dx,dy})=>{
rotateDelta[0] -= dx
rotateDelta[1] -= dy
updateView()
},
zoom: ({dy})=>{
zoomDelta += dy
updateView()
},
resize,
entities: (params)=>{
entities = params.entities
updateRender = params.scriptStats
updateView()
},
init: (params)=>{
if(params.canvas) startRenderer(params)
initialized = true
},
}
return {
// called from outside to pass mesasges into worker
postMessage: cmdHandler(handlers),
}
}
return (params)=>{
let { canvas, baseURI=(typeof document === 'undefined') ? '':document.location.toString(), scope='main', renderInWorker, render, callback=()=>{}, scriptUrl='demo-worker.js', alias, convertToSolids=false } = params
// by default 'render' messages go outside of this instance (result of modeling)
let sendToRender = callback
let scriptWorker, renderWorker
workerBaseURI = baseURI
const sendCmd = (params, transfer)=>{
if(params.worker === 'render')
sendToRender(params, transfer)
else if(params.worker === 'script')
scriptWorker.postMessage(params, transfer)
else{
// parameter definitions will arrive from scriptWorker
callback(params, transfer)
}
}
const updateSize = function({width,height}){
sendCmd({ action:'resize', worker:'render', width: canvas.offsetWidth, height: canvas.offsetHeight})
}
renderInWorker = !!(canvas && renderInWorker && canvas.transferControlToOffscreen)
const makeRenderWorkerHere = (scope === 'main' && canvas && !renderInWorker) || (scope === 'worker' && render)
// worker is in current thread
if(makeRenderWorkerHere){
renderWorker = makeRenderWorker({callback:sendCmd})
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
}
if(scope === 'main'){
// let extraScript = renderInWorker ? `,'https://unpkg.com/@jscad/regl-renderer'`:''
let script =`let baseURI = '${baseURI}'
importScripts(new URL('${scriptUrl}',baseURI))
let worker = jscadWorker({
baseURI: baseURI,
convertToSolids: ${convertToSolids},
scope:'worker',
callback:(params)=>self.postMessage(params),
render:${renderInWorker}
})
self.addEventListener('message', (e)=>worker.postMessage(e.data))
`
let blob = new Blob([script],{type: 'text/javascript'})
scriptWorker = new Worker(window.URL.createObjectURL(blob))
scriptWorker.addEventListener('message',(e)=>sendCmd(e.data))
scriptWorker.postMessage({action:'init', baseURI, alias})
if(renderInWorker) renderWorker = scriptWorker
if(canvas){
initCanvas(canvas, sendCmd)
window.addEventListener('resize',updateSize)
}
}else{
scriptWorker = makeScriptWorker({callback:sendCmd, convertToSolids})
callback({action:'workerInit',worker:'main'})
}
if(canvas){
// redirect 'render' messages to renderWorker
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
let width = canvas.width = canvas.clientWidth
let height = canvas.height = canvas.clientHeight
if(scope == 'main'){
const offscreen = renderInWorker ? canvas.transferControlToOffscreen() : canvas
renderWorker.postMessage({action:'init', worker:'render', canvas:offscreen, width, height}, [offscreen])
}
}
return {
updateSize,
updateParams:({params={}})=>sendCmd({ action:'updateParams', worker:'script', params}),
runScript: ({script,url=''})=>sendCmd({ action:'runScript', worker:'script', script, url}),
postMessage: sendCmd,
}
}
// multi purpose module
});

View File

@@ -44,28 +44,24 @@ const Routes = () => {
<Route notfound page={NotFoundPage} />
{/* Ownership enforced routes */}
<Route path="/u/{userName}/new" page={NewProjectPage} name="newProject" />
<Route path="/u/{userName}/new" page={NewPartPage} name="newPart" />
<Private unauthenticated="home" role="user">
<Route path="/u/{userName}/edit" page={EditUserPage} name="editUser" />
<Route path="/u/{userName}/{projectTitle}/edit" page={EditProjectPage} name="editProject" />
<Route path="/u/{userName}/{partTitle}/edit" page={EditPartPage} name="editPart" />
</Private>
{/* End ownership enforced routes */}
<Route path="/draft/{cadPackage}" page={DraftProjectPage} name="draftProject" />
<Route path="/draft" page={DraftPartPage} name="draftPart" />
<Route path="/u/{userName}" page={UserPage} name="user" />
<Route path="/u/{userName}/{projectTitle}" page={ProjectPage} name="project" />
<Route path="/u/{userName}/{projectTitle}/ide" page={IdeProjectPage} name="ide" />
<Route path="/u/{userName}/{projectTitle}/social-card" page={SocialCardPage} name="socialCard" />
<Route path="/u/{userName}/{partTitle}" page={PartPage} name="part" />
<Route path="/u/{userName}/{partTitle}/ide" page={IdePartPage} name="ide" />
<Private unauthenticated="home" role="admin">
<Route path="/admin/users" page={UsersPage} name="users" />
<Route path="/admin/projects" page={AdminProjectsPage} name="projects" />
<Route path="/admin/parts" page={AdminPartsPage} name="parts" />
<Route path="/admin/subject-access-requests/{id}/edit" page={EditSubjectAccessRequestPage} name="editSubjectAccessRequest" />
<Route path="/admin/subject-access-requests/{id}" page={SubjectAccessRequestPage} name="subjectAccessRequest" />
<Route path="/admin/subject-access-requests" page={SubjectAccessRequestsPage} name="subjectAccessRequests" />
{/* Retired for now but might want to bring it back, delete if older that I dunno late 2021 */}
{/* <Route path="/admin/email" page={AdminEmailPage} name="adminEmail" /> */}
</Private>
</Router>
)

1
app/web/src/cascade Submodule

Submodule app/web/src/cascade added at cd23a8e673

View File

@@ -1,12 +1,11 @@
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { useMutation, useFlash } from '@redwoodjs/web'
import { Link, routes } from '@redwoodjs/router'
import { QUERY } from 'src/components/AdminProjectsCell/AdminProjectsCell'
import { QUERY } from 'src/components/AdminPartsCell'
const DELETE_PROJECT_MUTATION_ADMIN = gql`
mutation DeleteProjectMutationAdmin($id: String!) {
deleteProject(id: $id) {
const DELETE_PART_MUTATION = gql`
mutation DeletePartMutation($id: String!) {
deletePart(id: $id) {
id
}
}
@@ -22,6 +21,10 @@ const truncate = (text) => {
return output
}
const jsonTruncate = (obj) => {
return truncate(JSON.stringify(obj, null, 2))
}
const timeTag = (datetime) => {
return (
<time dateTime={datetime} title={datetime}>
@@ -34,10 +37,11 @@ const checkboxInputTag = (checked) => {
return <input type="checkbox" checked={checked} disabled />
}
const AdminProjects = ({ projects }) => {
const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION_ADMIN, {
const AdminParts = ({ parts }) => {
const { addMessage } = useFlash()
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
onCompleted: () => {
toast.success('Project deleted.')
addMessage('Part deleted.', { classes: 'rw-flash-success' })
},
// This refetches the query on the list page. Read more about other ways to
// update the cache over here:
@@ -47,8 +51,8 @@ const AdminProjects = ({ projects }) => {
})
const onDeleteClick = (id) => {
if (confirm('Are you sure you want to delete project ' + id + '?')) {
deleteProject({ variables: { id } })
if (confirm('Are you sure you want to delete part ' + id + '?')) {
deletePart({ variables: { id } })
}
}
@@ -70,44 +74,44 @@ const AdminProjects = ({ projects }) => {
</tr>
</thead>
<tbody>
{projects.map((project) => (
<tr key={project.id}>
<td>{truncate(project.id)}</td>
<td>{truncate(project.title)}</td>
<td>{truncate(project.description)}</td>
<td>{truncate(project.code)}</td>
<td>{truncate(project.mainImage)}</td>
<td>{timeTag(project.createdAt)}</td>
<td>{timeTag(project.updatedAt)}</td>
<td>{truncate(project.userId)}</td>
<td>{checkboxInputTag(project.deleted)}</td>
{parts.map((part) => (
<tr key={part.id}>
<td>{truncate(part.id)}</td>
<td>{truncate(part.title)}</td>
<td>{truncate(part.description)}</td>
<td>{truncate(part.code)}</td>
<td>{truncate(part.mainImage)}</td>
<td>{timeTag(part.createdAt)}</td>
<td>{timeTag(part.updatedAt)}</td>
<td>{truncate(part.userId)}</td>
<td>{checkboxInputTag(part.deleted)}</td>
<td>
<nav className="rw-table-actions">
<Link
to={routes.project({
userName: project?.user?.userName,
projectTitle: project?.title,
to={routes.part({
userName: part?.user?.userName,
partTitle: part?.title,
})}
title={'Show project ' + project.id + ' detail'}
title={'Show part ' + part.id + ' detail'}
className="rw-button rw-button-small"
>
Show
</Link>
<Link
to={routes.editProject({
userName: project?.user?.userName,
projectTitle: project?.title,
to={routes.editPart({
userName: part?.user?.userName,
partTitle: part?.title,
})}
title={'Edit project ' + project.id}
title={'Edit part ' + part.id}
className="rw-button rw-button-small rw-button-blue"
>
Edit
</Link>
<a
href="#"
title={'Delete project ' + project.id}
title={'Delete part ' + part.id}
className="rw-button rw-button-small rw-button-red"
onClick={() => onDeleteClick(project.id)}
onClick={() => onDeleteClick(part.id)}
>
Delete
</a>
@@ -121,4 +125,4 @@ const AdminProjects = ({ projects }) => {
)
}
export default AdminProjects
export default AdminParts

View File

@@ -1,10 +1,10 @@
import { Link, routes } from '@redwoodjs/router'
import AdminProjects from 'src/components/AdminProjects/AdminProjects'
import AdminParts from 'src/components/AdminParts'
export const QUERY = gql`
query PROJECTS_ADMIN {
projects {
query PARTS {
parts {
id
title
description
@@ -26,14 +26,14 @@ export const Loading = () => <div>Loading...</div>
export const Empty = () => {
return (
<div className="rw-text-center">
{'No projects yet. '}
<Link to={routes.newProject()} className="rw-link">
{'No parts yet. '}
<Link to={routes.newPart()} className="rw-link">
{'Create one?'}
</Link>
</div>
)
}
export const Success = ({ projects }) => {
return <AdminProjects projects={projects} />
export const Success = ({ parts }) => {
return <AdminParts parts={parts} />
}

View File

@@ -0,0 +1,42 @@
import { getActiveClasses } from 'get-active-classes'
import { Link, routes } from '@redwoodjs/router'
import InputText from 'src/components/InputText'
const Breadcrumb = ({
userName,
partTitle,
onPartTitleChange,
className,
isInvalid,
}) => {
return (
<h3 className={getActiveClasses('text-2xl font-roboto', className)}>
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">
.
</div>
<span
className={getActiveClasses({
'text-gray-500': !onPartTitleChange,
'text-gray-400': onPartTitleChange,
})}
>
<Link to={routes.user({ userName })}>{userName}</Link>
</span>
<div className="w-1 inline-block bg-gray-400 text-gray-400 mx-3 transform -skew-x-20">
.
</div>
<InputText
value={partTitle}
onChange={onPartTitleChange}
isEditable={onPartTitleChange}
className={getActiveClasses('text-indigo-800 text-2xl', {
'-ml-2': !onPartTitleChange,
})}
isInvalid={isInvalid}
/>
</h3>
)
}
export default Breadcrumb

View File

@@ -0,0 +1,7 @@
import Breadcrumb from './Breadcrumb'
export const generated = () => {
return <Breadcrumb />
}
export default { title: 'Components/Breadcrumb' }

View File

@@ -1,11 +1,11 @@
import { render } from '@redwoodjs/testing'
import ProjectPage from './ProjectPage'
import Breadcrumb from './Breadcrumb'
describe('ProjectPage', () => {
describe('Breadcrumb', () => {
it('renders successfully', () => {
expect(() => {
render(<ProjectPage />)
render(<Breadcrumb />)
}).not.toThrow()
})
})

View File

@@ -19,7 +19,7 @@ const Button = ({
'text-red-600 bg-red-200 border border-red-600': type === 'danger',
'text-indigo-600': !type,
},
'flex items-center bg-opacity-50 rounded p-2 px-6',
'flex items-center bg-opacity-50 rounded-xl p-2 px-6',
{
'mx-px transform hover:-translate-y-px transition-all duration-150':
shouldAnimateHover && !disabled,
@@ -29,7 +29,7 @@ const Button = ({
onClick={onClick}
>
{children}
{iconName && <Svg className="w-6 ml-4" name={iconName} />}
<Svg className="w-6 ml-4" name={iconName} />
</button>
)
}

View File

@@ -1,30 +0,0 @@
import { ideTypeNameMap } from 'src/helpers/hooks/useIdeContext'
interface CadPackageProps {
cadPackage: string
className?: string
}
const CadPackage = ({ cadPackage, className = '' }: CadPackageProps) => {
const cadName = ideTypeNameMap[cadPackage] || ''
const isOpenScad = cadPackage === 'openscad'
const isCadQuery = cadPackage === 'cadquery'
return (
<div
className={
`grid grid-flow-col-dense items-center gap-2 cursor-default text-gray-100 ${
isOpenScad && 'bg-yellow-800'
} ${isCadQuery && 'bg-ch-blue-300'} bg-opacity-30 ` + className
}
>
<div
className={`${isOpenScad && 'bg-yellow-200'} ${
isCadQuery && 'bg-blue-800'
} w-5 h-5 rounded-full`}
/>
<div>{cadName}</div>
</div>
)
}
export default CadPackage

View File

@@ -1,193 +0,0 @@
import { useState } from 'react'
import { toast } from '@redwoodjs/web/toast'
import Popover from '@material-ui/core/Popover'
import Svg from 'src/components/Svg/Svg'
import Button from 'src/components/Button/Button'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob'
import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages'
import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
import { toJpeg } from 'html-to-image'
const anchorOrigin = {
vertical: 'bottom',
horizontal: 'center',
}
const transformOrigin = {
vertical: 'top',
horizontal: 'center',
}
const CaptureButton = ({
canEdit,
TheButton,
shouldUpdateImage,
projectTitle,
userName,
}) => {
const [captureState, setCaptureState] = useState<any>({})
const [anchorEl, setAnchorEl] = useState(null)
const [whichPopup, setWhichPopup] = useState(null)
const { state, project } = useIdeContext()
const ref = React.useRef<HTMLDivElement>(null)
const { updateProjectImages } = useUpdateProjectImages({})
const onCapture = async () => {
const threeInstance = state.threeInstance
const isOpenScadImage = state?.objectData?.type === 'png'
let imgBlob
let image64
if (!isOpenScadImage) {
imgBlob = canvasToBlob(threeInstance, { width: 500, height: 375 })
image64 = blobTo64(
await canvasToBlob(threeInstance, { width: 500, height: 522 })
)
} else {
imgBlob = state.objectData.data
image64 = blobTo64(state.objectData.data)
}
const config = {
image: await imgBlob,
currImage: project?.mainImage,
imageObjectURL: window.URL.createObjectURL(await imgBlob),
callback: uploadAndUpdateImage,
cloudinaryImgURL: '',
updated: false,
image64: await image64,
}
setCaptureState(config)
async function uploadAndUpdateImage() {
const upload = async () => {
const socialCard64 = toJpeg(ref.current, {
cacheBust: true,
quality: 0.7,
})
// uploading in two separate mutations because of the 100kb limit of the lambda functions
const imageUploadPromise1 = updateProjectImages({
variables: {
id: project?.id,
mainImage64: await config.image64,
},
})
const imageUploadPromise2 = updateProjectImages({
variables: {
id: project?.id,
socialCard64: await socialCard64,
},
})
return Promise.all([imageUploadPromise2, imageUploadPromise1])
}
const promise = upload()
toast.promise(promise, {
loading: 'Saving Image/s',
success: <b>Image/s saved!</b>,
error: <b>Problem saving.</b>,
})
const [{ data }] = await promise
return data?.updateProjectImages?.mainImage
}
// if there isn't a screenshot saved yet, just go ahead and save right away
if (shouldUpdateImage) {
config.cloudinaryImgURL = await uploadAndUpdateImage()
config.updated = true
setCaptureState(config)
}
}
const handleClick = ({ event, whichPopup }) => {
setAnchorEl(event.currentTarget)
setWhichPopup(whichPopup)
}
const handleClose = () => {
setAnchorEl(null)
setWhichPopup(null)
}
return (
<div>
{canEdit && (
<div>
<TheButton
onClick={async (event) => {
handleClick({ event, whichPopup: 'capture' })
onCapture()
}}
/>
<Popover
id={'capture-popover'}
open={whichPopup === 'capture'}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<div className="text-sm p-4 text-gray-500">
{!captureState ? (
'Loading...'
) : (
<div className="">
<div className="text-lg">Thumbnail</div>
<div
className="rounded"
style={{ width: 'fit-content', overflow: 'hidden' }}
>
<img src={captureState.imageObjectURL} className="w-32" />
</div>
</div>
)}
<div className="text-lg mt-4">Social Media Card</div>
<div className="rounded-lg shadow-md overflow-hidden">
<div
className="transform scale-50 origin-top-left"
style={{ width: '600px', height: '315px' }}
>
<div style={{ width: '1200px', height: '630px' }} ref={ref}>
<SocialCardCell
userName={userName}
projectTitle={projectTitle}
image64={captureState.image64}
/>
</div>
</div>
</div>
<div className="mt-4 text-indigo-800">
{captureState.currImage && !captureState.updated ? (
<Button
iconName="refresh"
className="shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-200 text-indigo-100 text-opacity-100 bg-opacity-80"
shouldAnimateHover
onClick={async () => {
const cloudinaryImg = await captureState.callback()
setCaptureState({
...captureState,
currImage: cloudinaryImg,
updated: true,
})
}}
>
Update Project Images
</Button>
) : (
<div className="flex justify-center mb-4">
<Svg
name="checkmark"
className="mr-2 w-6 text-indigo-600"
/>{' '}
Project Images Updated
</div>
)}
</div>
</div>
</Popover>
</div>
)}
</div>
)
}
export default CaptureButton

View File

@@ -1,271 +0,0 @@
import { useRender } from 'src/components/IdeWrapper/useRender'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { Switch } from '@headlessui/react'
import Svg from 'src/components/Svg/Svg'
import {
CadhubStringParam,
CadhubBooleanParam,
CadhubNumberParam,
} from './customizerConverter'
import { debounce } from 'lodash'
const Customizer = () => {
const [open, setOpen] = React.useState(false)
const [shouldLiveUpdate, setShouldLiveUpdate] = React.useState(false)
const { state, thunkDispatch } = useIdeContext()
const customizerParams = state?.customizerParams
const currentParameters = state?.currentParameters || {}
const handleRender = useRender()
const updateCustomizerParam = (paramName: string, paramValue: any) => {
const payload = {
...currentParameters,
[paramName]: paramValue,
}
thunkDispatch({ type: 'setCurrentCustomizerParams', payload })
shouldLiveUpdate && setTimeout(() => handleRender())
}
if (!customizerParams?.length) return null
return (
<div
className={`absolute inset-x-0 bottom-0 bg-ch-gray-600 bg-opacity-60 text-ch-gray-300 text-lg font-fira-sans ${
open ? 'h-2/3' : ''
}`}
>
<div className="flex justify-between px-6 py-2 items-center">
<div className="grid grid-flow-col-dense gap-6 items-center">
<button className="px-2" onClick={() => setOpen(!open)}>
<Svg
name="chevron-down"
className={`h-8 w-8 ${!open && 'transform rotate-180'}`}
/>
</button>
<div>Parameters</div>
</div>
{open && (
<>
<div className="flex items-center">
<div className="font-fira-sans text-sm mr-4">Auto Update</div>
<Switch
checked={shouldLiveUpdate}
onChange={(newValue) => {
if (newValue) handleRender()
setShouldLiveUpdate(newValue)
}}
className={`${
shouldLiveUpdate ? 'bg-ch-purple-600' : 'bg-ch-gray-300'
} relative inline-flex items-center h-6 rounded-full w-11 mr-6`}
>
<span
className={`${
shouldLiveUpdate ? 'translate-x-6' : 'translate-x-1'
} inline-block w-4 h-4 transform bg-white rounded-full`}
/>
</Switch>
<button
className={`px-4 py-1 rounded bg-ch-gray-300 text-ch-gray-800 ${
shouldLiveUpdate && 'bg-opacity-30 cursor-default'
}`}
onClick={handleRender}
disabled={shouldLiveUpdate}
>
Update
</button>
</div>
</>
)}
</div>
<div className={`${open ? 'h-full pb-32' : 'h-0'} overflow-y-auto px-12`}>
<div>
{customizerParams.map((param, index) => {
const otherProps = {
value: currentParameters[param.name],
onChange: (value) => updateCustomizerParam(param.name, value),
}
if (param.type === 'string') {
return <StringParam key={index} param={param} {...otherProps} />
} else if (param.type === 'number') {
return <NumberParam key={index} param={param} {...otherProps} />
} else if (param.type === 'boolean') {
return <BooleanParam key={index} param={param} {...otherProps} />
}
return <div key={index}>{JSON.stringify(param)}</div>
})}
</div>
</div>
</div>
)
}
export default Customizer
function CustomizerParamBase({
name,
caption,
children,
}: {
name: string
caption: string
children: React.ReactNode
}) {
return (
<li
className="grid items-center my-2"
style={{ gridTemplateColumns: 'auto 8rem' }}
>
<div className=" text-sm font-fira-sans">
<div className="font-bold text-base">{name}</div>
<div>{caption}</div>
</div>
<div className="w-full">{children}</div>
</li>
)
}
function BooleanParam({
param,
value,
onChange,
}: {
param: CadhubBooleanParam
value: any
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<Switch
checked={value}
onChange={(newValue) => {
onChange(newValue)
}}
className={`${
value ? 'bg-ch-gray-300' : 'bg-ch-gray-600'
} relative inline-flex items-center h-6 rounded-full w-11 mr-6 border border-ch-gray-300`}
>
<span
className={`${
value ? 'translate-x-6' : 'translate-x-1'
} inline-block w-4 h-4 transform bg-white rounded-full`}
/>
</Switch>
</CustomizerParamBase>
)
}
function StringParam({
param,
value,
onChange,
}: {
param: CadhubStringParam
value: any
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<input
className="bg-transparent h-8 border border-ch-gray-300 px-2 text-sm w-full"
type="text"
value={value}
placeholder={param.placeholder}
onChange={({ target }) => onChange(target?.value)}
/>
</CustomizerParamBase>
)
}
function NumberParam({
param,
value,
onChange,
}: {
param: CadhubNumberParam
value: any
onChange: Function
}) {
const [isFocused, isFocusedSetter] = React.useState(false)
const [localValue, localValueSetter] = React.useState(0)
const [isLocked, isLockedSetter] = React.useState(false)
const [pixelsDragged, pixelsDraggedSetter] = React.useState(0)
const handleRender = useRender()
const liveRenderHandler = debounce((a) => handleRender(a), 250)
const step = param.step || 1
const live = false // TODO get from param
let decimal = 0
if('decimal' in param){
decimal = param.decimal
}else{
let str = String(step)
const idx = str.indexOf('.')
if(idx !== -1) decimal = str.length - idx - 1
}
const commitChange = () => {
let num = localValue
if (typeof param.step === 'number') {
num = Math.round(num / step) * step
}
if (typeof param.min === 'number') {
num = Math.max(param.min, num)
}
if (typeof param.max === 'number') {
num = Math.min(param.max, num)
}
num = Number(num.toFixed(decimal))
localValueSetter(num)
onChange(num)
}
React.useEffect(() => {
if (!isFocused) commitChange()
}, [isFocused])
React.useEffect(() => localValueSetter(value), [value])
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<div className="flex h-8 border border-ch-gray-300">
<input
className={`bg-transparent px-2 text-sm w-full ${
(param.max && param.max < localValue) ||
(param.min && param.min > localValue)
? 'text-red-500'
: ''
}`}
type="number"
value={localValue}
onFocus={() => isFocusedSetter(true)}
onBlur={() => isFocusedSetter(false)}
onKeyDown={({ key }) => key === 'Enter' && commitChange()}
onChange={({ target }) => {
const num = Number(target?.value)
localValueSetter(num)
}}
max={param.max}
min={param.min}
step={step}
/>
<div
className="w-6 border-l border-ch-gray-500 items-center hidden md:flex"
style={{ cursor: 'ew-resize' }}
onMouseDown={({ target }) => {
isLockedSetter(true)
target?.requestPointerLock?.()
pixelsDraggedSetter(localValue)
}}
onMouseUp={() => {
isLockedSetter(false)
document?.exitPointerLock?.()
commitChange()
}}
onMouseMove={({ movementX }) => {
if (isLocked && movementX) {
pixelsDraggedSetter(pixelsDragged + (movementX * step) / 8) // one step per 8 pixels
const decimalFixed = Number((Math.round(pixelsDragged / step) * step).toFixed(decimal))
localValueSetter(decimalFixed)
if(live) liveRenderHandler({[param.name]: decimalFixed})
}
}}
>
<Svg className="w-6" name="switch-horizontal" />
</div>
</div>
</CustomizerParamBase>
)
}

View File

@@ -1,120 +0,0 @@
// CadHub
type CadhubTypeNames = 'number' | 'string' | 'boolean'
interface CadhubParamBase {
type: CadhubTypeNames
caption: string
name: string
}
export interface CadhubStringParam extends CadhubParamBase {
type: 'string'
initial: string
placeholder?: string
maxLength?: number
}
export interface CadhubBooleanParam extends CadhubParamBase {
type: 'boolean'
initial?: boolean
}
export interface CadhubNumberParam extends CadhubParamBase {
type: 'number'
initial: number
min?: number
max?: number
step?: number
decimal?: number
}
export type CadhubParams =
| CadhubStringParam
| CadhubBooleanParam
| CadhubNumberParam
// OpenSCAD
const openscadValues = `
// slider widget for number with max. value
sliderWithMax =34; // [50]
// slider widget for number in range
sliderWithRange =34; // [10:100]
//step slider for number
stepSlider=2; //[0:5:100]
// slider widget for number in range
sliderCentered =0; // [-10:0.1:10]
// spinbox with step size 1
Spinbox= 5;
// Text box for string
String="hello";
// Text box for string with length 8
String2="length"; //8
//description
Variable = true;
`
const openscadConverted: CadhubParams[] = [
{
type: 'number',
name: 'sliderWithMax',
caption: 'slider widget for number with max. value',
initial: 34,
step: 1,
max: 50,
},
{
type: 'number',
name: 'sliderWithRange',
caption: 'slider widget for number in range',
initial: 34,
step: 1,
min: 10,
max: 100,
},
{
type: 'number',
name: 'stepSlider',
caption: 'step slider for number',
initial: 2,
step: 5,
min: 0,
max: 100,
},
{
type: 'number',
name: 'sliderCentered',
caption: 'slider widget for number in range',
initial: 0,
step: 0.1,
min: -10,
max: 10,
},
{
type: 'number',
name: 'Spinbox',
caption: 'spinbox with step size 1',
initial: 5,
step: 1,
},
{
type: 'string',
name: 'String',
caption: 'Text box for string',
initial: 'hello',
},
{
type: 'string',
name: 'String2',
caption: 'Text box for string with length 8',
initial: 'length',
maxLength: 8,
},
{ type: 'boolean', name: 'Variable', caption: 'description', initial: true },
]

View File

@@ -1,31 +0,0 @@
let timeoutId = 0
const DelayedPingAnimation = ({
isLoading: isLoading,
}: {
isLoading: boolean
}) => {
const [showLoading, setShowLoading] = React.useState(false)
React.useEffect(() => {
if (!isLoading && showLoading) {
setShowLoading(isLoading)
clearTimeout(timeoutId)
} else if (isLoading && !showLoading) {
timeoutId = setTimeout(() => {
setShowLoading(isLoading)
}, 300) as unknown as number
} else if (!isLoading) {
setShowLoading(isLoading)
clearTimeout(timeoutId)
}
}, [isLoading])
if (showLoading && isLoading)
return (
<div className="inset-0 absolute flex items-center justify-center">
<div className="h-16 w-16 bg-pink-600 rounded-full animate-ping"></div>
</div>
)
return null
}
export default DelayedPingAnimation

View File

@@ -1,5 +1,4 @@
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { useMutation, useFlash } from '@redwoodjs/web'
import { navigate, routes } from '@redwoodjs/router'
import SubjectAccessRequestForm from 'src/components/SubjectAccessRequestForm'
@@ -34,12 +33,15 @@ const UPDATE_SUBJECT_ACCESS_REQUEST_MUTATION = gql`
export const Loading = () => <div>Loading...</div>
export const Success = ({ subjectAccessRequest }) => {
const { addMessage } = useFlash()
const [updateSubjectAccessRequest, { loading, error }] = useMutation(
UPDATE_SUBJECT_ACCESS_REQUEST_MUTATION,
{
onCompleted: () => {
navigate(routes.subjectAccessRequests())
toast.success('SubjectAccessRequest updated.')
addMessage('SubjectAccessRequest updated.', {
classes: 'rw-flash-success',
})
},
}
)

View File

@@ -1,11 +1,10 @@
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { useMutation, useFlash } from '@redwoodjs/web'
import { navigate, routes } from '@redwoodjs/router'
import UserProfile from 'src/components/UserProfile'
export const QUERY = gql`
query FIND_USER_BY_USERNAME($userName: String!) {
query FIND_USER_BY_ID($userName: String!) {
user: userName(userName: $userName) {
id
userName
@@ -23,9 +22,6 @@ const UPDATE_USER_MUTATION = gql`
updateUserByUserName(userName: $userName, input: $input) {
id
userName
bio
name
image
}
}
`
@@ -37,10 +33,11 @@ export const Empty = () => <div className="h-full">Empty</div>
export const Failure = ({ error }) => <div>Error: {error.message}</div>
export const Success = ({ user, refetch, variables: { isEditable } }) => {
const { addMessage } = useFlash()
const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, {
onCompleted: ({ updateUserByUserName }) => {
navigate(routes.user({ userName: updateUserByUserName.userName }))
toast.success('User updated.')
addMessage('User updated.', { classes: 'rw-flash-success' })
},
})

View File

@@ -1,6 +1,6 @@
// Define your own mock data here:
export const standard = (/* vars, { ctx, req } */) => ({
ideProject: {
editUser: {
id: 42,
},
})

View File

@@ -1,5 +1,5 @@
import { Loading, Empty, Failure, Success } from './ProjectsOfUserCell'
import { standard } from './ProjectsOfUserCell.mock'
import { Loading, Empty, Failure, Success } from './EditUserCell'
import { standard } from './EditUserCell.mock'
export const loading = () => {
return Loading ? <Loading /> : null
@@ -17,4 +17,4 @@ export const success = () => {
return Success ? <Success {...standard()} /> : null
}
export default { title: 'Cells/ProjectsOfUserCell' }
export default { title: 'Cells/EditUserCell' }

View File

@@ -1,8 +1,8 @@
import { render, screen } from '@redwoodjs/testing'
import { Loading, Empty, Failure, Success } from './ProjectReactionsCell'
import { standard } from './ProjectReactionsCell.mock'
import { Loading, Empty, Failure, Success } from './EditUserCell'
import { standard } from './EditUserCell.mock'
describe('ProjectReactionsCell', () => {
describe('EditUserCell', () => {
test('Loading renders successfully', () => {
render(<Loading />)
// Use screen.debug() to see output
@@ -20,7 +20,7 @@ describe('ProjectReactionsCell', () => {
})
test('Success renders successfully', async () => {
render(<Success projectReactions={standard().projectReactions} />)
render(<Success editUser={standard().editUser} />)
expect(screen.getByText(/42/i)).toBeInTheDocument()
})
})

View File

@@ -1,109 +0,0 @@
import { useState } from 'react'
import { toast } from '@redwoodjs/web/toast'
import { Link, navigate, routes } from '@redwoodjs/router'
import { useUpdateProject } from 'src/helpers/hooks/useUpdateProject'
import Svg from 'src/components/Svg/Svg'
interface EditableProjectTitleProps {
id: string
userName: string
projectTitle: string
canEdit: boolean
shouldRouteToIde: boolean
}
const EditableProjectTitle = ({
id,
userName,
projectTitle,
canEdit,
shouldRouteToIde,
}: EditableProjectTitleProps) => {
const [inEditMode, setInEditMode] = useState(false)
const [newTitle, setNewTitle] = useState(projectTitle)
const inputRef = React.useRef(null)
const { updateProject, loading, error } = useUpdateProject({
onCompleted: ({ updateProject }) => {
const routeVars = {
userName: updateProject.user.userName,
projectTitle: updateProject.title,
}
navigate(
shouldRouteToIde ? routes.ide(routeVars) : routes.project(routeVars)
)
toast.success('Project updated.')
},
})
const onTitleChange = ({ target }) => {
if (target.value.length > 25) {
toast.error('Titles must be 25 or less characters')
}
setNewTitle(target.value.replace(/([^a-zA-Z\d_:])/g, '-').slice(0, 25))
}
return (
<>
{!inEditMode && (
<>
<Link
to={routes.project({
userName,
projectTitle,
})}
className="pl-4"
>
/{projectTitle}
</Link>
{canEdit && (
<button
onClick={() => {
setInEditMode(true)
setTimeout(() => inputRef?.current?.focus())
}}
>
<Svg name="pencil-solid" className="h-4 w-4 ml-4 mb-2" />
</button>
)}
</>
)}
{inEditMode && (
<>
<span className="flex items-center ml-4 border border-ch-gray-300 rounded-sm">
<span className="ml-1">/</span>
<input
className="pl-1 w-64 bg-ch-gray-900"
value={newTitle}
onChange={onTitleChange}
ref={inputRef}
onBlur={() =>
setTimeout(() => {
setInEditMode(false)
setNewTitle(projectTitle)
}, 300)
}
/>
</span>
<div className="flex items-center h-full">
<button
className="ml-4 grid grid-flow-col-dense p-px px-2 gap-2 bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 rounded-sm border border-ch-purple-400"
id="rename-button"
onClick={() =>
updateProject({ variables: { id, input: { title: newTitle } } })
}
>
<Svg
name="check"
className="w-6 h-6 text-ch-purple-500"
strokeWidth={3}
/>
<span>Rename</span>
</button>
</div>
</>
)}
</>
)
}
export default EditableProjectTitle

View File

@@ -1,133 +0,0 @@
import { Menu } from '@headlessui/react'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import Svg from 'src/components/Svg/Svg'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { makeStlDownloadHandler, PullTitleFromFirstLine } from './helpers'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import CadPackage from 'src/components/CadPackage/CadPackage'
const EditorMenu = () => {
const handleRender = useRender()
const saveCode = useSaveCode()
const { state, thunkDispatch } = useIdeContext()
const onRender = () => {
handleRender()
saveCode({ code: state.code })
}
const handleStlDownload = makeStlDownloadHandler({
type: state.objectData?.type,
ideType: state.ideType,
geometry: state.objectData?.data,
quality: state.objectData?.quality,
fileName: PullTitleFromFirstLine(state.code || ''),
thunkDispatch,
})
return (
<div className="flex justify-between bg-ch-gray-760 text-gray-100">
<div className="flex items-center h-9 w-full cursor-grab">
<div className=" text-ch-gray-760 bg-ch-gray-300 cursor-grab px-2 h-full flex items-center">
<Svg name="drag-grid" className="w-4 p-px" />
</div>
<div className="grid grid-flow-col-dense gap-6 px-5">
<FileDropdown
handleRender={onRender}
handleStlDownload={handleStlDownload}
/>
<button className="cursor-not-allowed" disabled>
Edit
</button>
<ViewDropdown
handleLayoutReset={() => thunkDispatch({ type: 'resetLayout' })}
/>
</div>
<button
className="text-ch-gray-300 h-full cursor-not-allowed"
aria-label="editor settings"
disabled
>
<Svg name="gear" className="w-6 p-px" />
</button>
</div>
<CadPackage cadPackage={state.ideType} className="px-3" />
</div>
)
}
export default EditorMenu
function FileDropdown({ handleRender, handleStlDownload }) {
return (
<Dropdown name="File">
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1`}
onClick={handleRender}
>
Save &amp; Render{' '}
<span className="text-gray-400 pl-4">
{/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? (
<>
<Svg
name="mac-cmd-key"
className="h-3 w-3 inline-block text-left"
/>
S
</>
) : (
'Ctrl S'
)}
</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1 text-left`}
onClick={handleStlDownload}
>
Download STL
</button>
)}
</Menu.Item>
</Dropdown>
)
}
function ViewDropdown({ handleLayoutReset }) {
return (
<Dropdown name="View">
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1`}
onClick={handleLayoutReset}
>
Reset layout
</button>
)}
</Menu.Item>
</Dropdown>
)
}
function Dropdown({
name,
children,
}: {
name: string
children: React.ReactNode
}) {
return (
<div className="relative">
<Menu>
<Menu.Button className="text-gray-100">{name}</Menu.Button>
<Menu.Items className="absolute flex flex-col mt-4 bg-ch-gray-760 rounded text-gray-100 overflow-hidden whitespace-nowrap border border-ch-gray-700">
{children}
</Menu.Items>
</Menu>
</div>
)
}

View File

@@ -1,84 +0,0 @@
import { flow, identity } from 'lodash/fp'
import { fileSave } from 'browser-fs-access'
import { MeshBasicMaterial, Mesh, Scene } from 'three'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
import { requestRender, State } from 'src/helpers/hooks/useIdeState'
export const PullTitleFromFirstLine = (code = '') => {
const firstLine = code.split('\n').filter(identity)[0] || ''
if (!(firstLine.startsWith('//') || firstLine.startsWith('#'))) {
return 'object.stl'
}
return (
(firstLine.replace(/^(\/\/|#)\s*(.+)/, (_, __, titleWithSpaces) =>
titleWithSpaces.replaceAll(/\s/g, '-')
) || 'object') + '.stl'
)
}
interface makeStlDownloadHandlerArgs {
geometry: any
fileName: string
type: State['objectData']['type']
ideType: State['ideType']
thunkDispatch: (a: any) => any
quality: State['objectData']['quality']
}
export const makeStlDownloadHandler =
({
geometry,
fileName,
type,
thunkDispatch,
quality,
ideType,
}: makeStlDownloadHandlerArgs) =>
() => {
const makeStlBlobFromMesh = flow(
(...meshes) => new Scene().add(...meshes),
(scene) => new STLExporter().parse(scene),
(stl) =>
new Blob([stl], {
type: 'text/plain',
})
)
const makeStlBlobFromGeo = flow(
(geo) => new Mesh(geo, new MeshBasicMaterial()),
(mesh) => makeStlBlobFromMesh(mesh)
)
const saveFile = (blob) => {
fileSave(blob, {
fileName,
extensions: ['.stl'],
})
}
if (geometry) {
if (
type === 'geometry' &&
(quality === 'high' || ideType === 'openscad')
) {
saveFile(makeStlBlobFromGeo(geometry))
} else if (ideType == 'jscad') {
saveFile(makeStlBlobFromMesh(...geometry))
} else {
thunkDispatch((dispatch, getState) => {
const state = getState()
const specialCadProcess = ideType === 'openscad' && 'stl'
dispatch({ type: 'setLoading' })
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
quality: 'high',
specialCadProcess,
parameters: state.currentParameters,
}).then(
(result) => result && saveFile(makeStlBlobFromGeo(result.data))
)
})
}
}
}

View File

@@ -3,7 +3,7 @@ import { getActiveClasses } from 'get-active-classes'
import Popover from '@material-ui/core/Popover'
import { useAuth } from '@redwoodjs/auth'
import Svg from 'src/components/Svg/Svg'
import Svg from 'src/components/Svg'
const emojiMenu = ['❤️', '👍', '😄', '🙌']
// const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌']
@@ -20,7 +20,7 @@ const EmojiReaction = ({
emotes,
userEmotes,
onEmote = () => {},
onShowProjectReactions,
onShowPartReactions,
className,
}) => {
const { currentUser } = useAuth()
@@ -57,41 +57,42 @@ const EmojiReaction = ({
return (
<>
<div
className={getActiveClasses('relative overflow-hidden pt-1', className)}
className={getActiveClasses(
'h-10 relative overflow-hidden py-4',
className
)}
>
<div className="z-10 flex items-center h-10">
<div className="absolute left-0 w-8 inset-y-0 z-10 flex items-center bg-gray-100">
<div
className="h-full w-10 mr-4"
className="h-8 w-8 relative"
aria-describedby={popoverId}
onClick={togglePopover}
>
<button className="bg-ch-gray-600 w-full h-full flex justify-center items-center shadow-md hover:shadow-lg transform hover:-translate-y-px transition-all duration-150 rounded">
<Svg className="w-8 text-ch-gray-300" name="dots-vertical" />
<button className="bg-gray-200 border-2 m-px w-full h-full border-gray-300 rounded-full flex justify-center items-center shadow-md hover:shadow-lg hover:border-indigo-200 transform hover:-translate-y-px transition-all duration-150">
<Svg
className="h-8 w-8 pt-px mt-px text-gray-500"
name="dots-vertical"
/>
</button>
</div>
</div>
<div className="whitespace-nowrap absolute right-0 inset-y-0 flex items-center flex-row-reverse">
{(emotes.length ? emotes : noEmotes).map((emote, i) => (
<span
className={getActiveClasses(
'tracking-wide border border-transparent hover:border-ch-gray-300 h-full p-1 px-4 transform hover:-translate-y-px transition-all duration-150 flex items-center rounded',
{
'bg-ch-gray-500 text-ch-gray-900':
currentUser && userEmotes?.includes(emote.emoji),
'bg-ch-gray-600': !(
currentUser && userEmotes?.includes(emote.emoji)
),
}
'rounded-full tracking-wide hover:bg-indigo-100 p-1 mx-px transform hover:-translate-y-px transition-all duration-150 border-indigo-400',
{ border: currentUser && userEmotes?.includes(emote.emoji) }
)}
style={textShadow}
key={`${emote.emoji}--${i}`}
onClick={() => handleEmojiClick(emote.emoji)}
>
<span className="text-lg pr-2">{emote.emoji}</span>
<span className="text-sm font-fira-code">{emote.count}</span>
<span className="text-lg pr-1">{emote.emoji}</span>
<span className="text-sm font-ropa-sans">{emote.count}</span>
</span>
))}
</div>
<div className="whitespace-nowrap flex items-center flex-row-reverse"></div>
</div>
<Popover
id={popoverId}
@@ -120,7 +121,7 @@ const EmojiReaction = ({
</button>
))}
</div>
<button className="text-gray-700" onClick={onShowProjectReactions}>
<button className="text-gray-700" onClick={onShowPartReactions}>
View Reactions
</button>
</div>

Some files were not shown because too many files have changed in this diff Show More