mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-11 03:24:16 +00:00
added marketplace_server
This commit is contained in:
parent
13c968ecea
commit
c3a9ffe80f
1
packages/marketplace_server/.gitignore
vendored
Normal file
1
packages/marketplace_server/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/static
|
49
packages/marketplace_server/package.json
Normal file
49
packages/marketplace_server/package.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "@comty/marketplace_server",
|
||||||
|
"version": "0.45.2",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "corenode-cli build",
|
||||||
|
"dev": "cross-env NODE_ENV=development nodemon --ignore dist/ --exec corenode-node ./src/index.js"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"7zip-min": "^1.4.4",
|
||||||
|
"@corenode/utils": "0.28.26",
|
||||||
|
"@foxify/events": "^2.1.0",
|
||||||
|
"@octokit/rest": "^19.0.7",
|
||||||
|
"axios": "^1.2.5",
|
||||||
|
"bcrypt": "^5.1.0",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
|
"connect-mongo": "^4.6.0",
|
||||||
|
"content-range": "^2.0.2",
|
||||||
|
"corenode": "0.28.26",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"formidable": "^2.1.1",
|
||||||
|
"hyper-express": "^6.5.9",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"linebridge": "0.15.12",
|
||||||
|
"live-directory": "^3.0.3",
|
||||||
|
"luxon": "^3.2.1",
|
||||||
|
"merge-files": "^0.1.2",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"minio": "^7.0.32",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"moment-timezone": "^0.5.40",
|
||||||
|
"mongoose": "^6.9.0",
|
||||||
|
"normalize-url": "^8.0.0",
|
||||||
|
"p-map": "^6.0.0",
|
||||||
|
"p-queue": "^7.3.4",
|
||||||
|
"redis": "^4.6.6",
|
||||||
|
"sharp": "^0.31.3",
|
||||||
|
"split-chunk-merge": "^1.0.0",
|
||||||
|
"sucrase": "^3.32.0",
|
||||||
|
"uglify-js": "^3.17.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.3.7",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"mocha": "^10.2.0",
|
||||||
|
"nodemon": "^2.0.15"
|
||||||
|
}
|
||||||
|
}
|
119
packages/marketplace_server/src/api.js
Normal file
119
packages/marketplace_server/src/api.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
import DbManager from "@classes/DbManager"
|
||||||
|
import RedisClient from "@classes/RedisClient"
|
||||||
|
import StorageClient from "@classes/StorageClient"
|
||||||
|
|
||||||
|
import hyperexpress from "hyper-express"
|
||||||
|
|
||||||
|
import pkg from "../package.json"
|
||||||
|
|
||||||
|
export default class WidgetsAPI {
|
||||||
|
server = global.server = new hyperexpress.Server()
|
||||||
|
|
||||||
|
listenIp = process.env.HTTP_LISTEN_IP ?? "0.0.0.0"
|
||||||
|
listenPort = process.env.HTTP_LISTEN_PORT ?? 3040
|
||||||
|
|
||||||
|
internalRouter = new hyperexpress.Router()
|
||||||
|
|
||||||
|
db = new DbManager()
|
||||||
|
|
||||||
|
redis = global.redis = RedisClient()
|
||||||
|
|
||||||
|
storage = global.storage = StorageClient()
|
||||||
|
|
||||||
|
async __registerControllers() {
|
||||||
|
let controllersPath = fs.readdirSync(path.resolve(__dirname, "controllers"))
|
||||||
|
|
||||||
|
for await (const controllerPath of controllersPath) {
|
||||||
|
const controller = require(path.resolve(__dirname, "controllers", controllerPath)).default
|
||||||
|
|
||||||
|
if (!controller) {
|
||||||
|
console.error(`Controller ${controllerPath} not found.`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = controller(new hyperexpress.Router())
|
||||||
|
|
||||||
|
if (!handler) {
|
||||||
|
console.error(`Controller ${controllerPath} returning not valid handler.`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.internalRouter.use(handler.path ?? "/", handler.router)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async __registerInternalMiddlewares() {
|
||||||
|
let middlewaresPath = fs.readdirSync(path.resolve(__dirname, "useMiddlewares"))
|
||||||
|
|
||||||
|
for await (const middlewarePath of middlewaresPath) {
|
||||||
|
const middleware = require(path.resolve(__dirname, "useMiddlewares", middlewarePath)).default
|
||||||
|
|
||||||
|
if (!middleware) {
|
||||||
|
console.error(`Middleware ${middlewarePath} not found.`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server.use(middleware)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__registerInternalRoutes() {
|
||||||
|
this.server.get("/", (req, res) => {
|
||||||
|
return res.status(200).json({
|
||||||
|
name: pkg.name,
|
||||||
|
version: pkg.version,
|
||||||
|
routes: this.__getRegisteredRoutes()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.server.any("*", (req, res) => {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "Not found",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
__getRegisteredRoutes() {
|
||||||
|
return this.internalRouter.routes.map((route) => {
|
||||||
|
return {
|
||||||
|
method: route.method,
|
||||||
|
path: route.pattern,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
const startHrTime = process.hrtime()
|
||||||
|
|
||||||
|
// initialize clients
|
||||||
|
await this.db.initialize()
|
||||||
|
await this.redis.initialize()
|
||||||
|
await this.storage.initialize()
|
||||||
|
|
||||||
|
// register controllers & middlewares
|
||||||
|
await this.__registerInternalRoutes()
|
||||||
|
await this.__registerControllers()
|
||||||
|
await this.__registerInternalMiddlewares()
|
||||||
|
|
||||||
|
// use internal router
|
||||||
|
this.server.use(this.internalRouter)
|
||||||
|
|
||||||
|
// start server
|
||||||
|
await this.server.listen(this.listenPort, this.listenIp)
|
||||||
|
|
||||||
|
// calculate elapsed time
|
||||||
|
const elapsedHrTime = process.hrtime(startHrTime)
|
||||||
|
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
|
||||||
|
|
||||||
|
// log server started
|
||||||
|
console.log(`🚀 Server started ready on \n\t - http://${this.listenIp}:${this.listenPort} \n\t - Tooks ${elapsedTimeInMs}ms`)
|
||||||
|
}
|
||||||
|
}
|
58
packages/marketplace_server/src/classes/DbManager/index.js
Executable file
58
packages/marketplace_server/src/classes/DbManager/index.js
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
import mongoose from "mongoose"
|
||||||
|
|
||||||
|
function getConnectionConfig(obj) {
|
||||||
|
const { DB_USER, DB_DRIVER, DB_NAME, DB_PWD, DB_HOSTNAME, DB_PORT } = obj
|
||||||
|
|
||||||
|
let auth = [
|
||||||
|
DB_DRIVER ?? "mongodb",
|
||||||
|
"://",
|
||||||
|
]
|
||||||
|
|
||||||
|
if (DB_USER && DB_PWD) {
|
||||||
|
auth.push(`${DB_USER}:${DB_PWD}@`)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.push(DB_HOSTNAME ?? "localhost")
|
||||||
|
auth.push(`:${DB_PORT ?? "27017"}`)
|
||||||
|
|
||||||
|
if (DB_USER) {
|
||||||
|
auth.push("/?authMechanism=DEFAULT")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth = auth.join("")
|
||||||
|
|
||||||
|
return [
|
||||||
|
auth,
|
||||||
|
{
|
||||||
|
dbName: DB_NAME,
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DBManager {
|
||||||
|
initialize = async (config) => {
|
||||||
|
console.log("🔌 Connecting to DB...")
|
||||||
|
|
||||||
|
const dbConfig = getConnectionConfig(config ?? process.env)
|
||||||
|
|
||||||
|
mongoose.set("strictQuery", false)
|
||||||
|
|
||||||
|
const connection = await mongoose.connect(...dbConfig)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(`❌ Failed to connect to DB, retrying...\n`)
|
||||||
|
console.log(error)
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// this.initialize()
|
||||||
|
// }, 1000)
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (connection) {
|
||||||
|
console.log(`✅ Connected to DB.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
packages/marketplace_server/src/classes/RedisClient/index.js
Normal file
44
packages/marketplace_server/src/classes/RedisClient/index.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { createClient } from "redis"
|
||||||
|
|
||||||
|
function composeURL() {
|
||||||
|
// support for auth
|
||||||
|
let url = "redis://"
|
||||||
|
|
||||||
|
if (process.env.REDIS_PASSWORD && process.env.REDIS_USERNAME) {
|
||||||
|
url += process.env.REDIS_USERNAME + ":" + process.env.REDIS_PASSWORD + "@"
|
||||||
|
}
|
||||||
|
|
||||||
|
url += process.env.REDIS_HOST ?? "localhost"
|
||||||
|
|
||||||
|
if (process.env.REDIS_PORT) {
|
||||||
|
url += ":" + process.env.REDIS_PORT
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
let client = createClient({
|
||||||
|
url: composeURL(),
|
||||||
|
})
|
||||||
|
|
||||||
|
client.initialize = async () => {
|
||||||
|
console.log("🔌 Connecting to Redis client...")
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle when client disconnects unexpectedly to avoid main crash
|
||||||
|
client.on("error", (error) => {
|
||||||
|
console.error("❌ Redis client error:", error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle when client connects
|
||||||
|
client.on("connect", () => {
|
||||||
|
console.log("✅ Redis client connected.")
|
||||||
|
})
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
94
packages/marketplace_server/src/classes/StorageClient/index.js
Executable file
94
packages/marketplace_server/src/classes/StorageClient/index.js
Executable file
@ -0,0 +1,94 @@
|
|||||||
|
const Minio = require("minio")
|
||||||
|
|
||||||
|
export const generateDefaultBucketPolicy = (payload) => {
|
||||||
|
const { bucketName } = payload
|
||||||
|
|
||||||
|
if (!bucketName) {
|
||||||
|
throw new Error("bucketName is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: [
|
||||||
|
{
|
||||||
|
Action: [
|
||||||
|
"s3:GetObject"
|
||||||
|
],
|
||||||
|
Effect: "Allow",
|
||||||
|
Principal: {
|
||||||
|
AWS: [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
Resource: [
|
||||||
|
`arn:aws:s3:::${bucketName}/*`
|
||||||
|
],
|
||||||
|
Sid: ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StorageClient extends Minio.Client {
|
||||||
|
constructor(options) {
|
||||||
|
super(options)
|
||||||
|
|
||||||
|
this.defaultBucket = String(options.defaultBucket)
|
||||||
|
this.defaultRegion = String(options.defaultRegion)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeRemoteURL = (key) => {
|
||||||
|
return `${this.protocol}//${this.host}:${this.port}/${this.defaultBucket}/${key}`
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultBucketPolicy = async (bucketName) => {
|
||||||
|
const policy = generateDefaultBucketPolicy({ bucketName })
|
||||||
|
|
||||||
|
return this.setBucketPolicy(bucketName, JSON.stringify(policy))
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize = async () => {
|
||||||
|
console.log("🔌 Checking if storage client have default bucket...")
|
||||||
|
|
||||||
|
// check connection with s3
|
||||||
|
const bucketExists = await this.bucketExists(this.defaultBucket).catch(() => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!bucketExists) {
|
||||||
|
console.warn("🪣 Default bucket not exists! Creating new bucket...")
|
||||||
|
|
||||||
|
await this.makeBucket(this.defaultBucket, "s3")
|
||||||
|
|
||||||
|
// set default bucket policy
|
||||||
|
await this.setDefaultBucketPolicy(this.defaultBucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if default bucket policy exists
|
||||||
|
const bucketPolicy = await this.getBucketPolicy(this.defaultBucket).catch(() => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!bucketPolicy) {
|
||||||
|
// set default bucket policy
|
||||||
|
await this.setDefaultBucketPolicy(this.defaultBucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Storage client is ready.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createStorageClientInstance = (options) => {
|
||||||
|
return new StorageClient({
|
||||||
|
...options,
|
||||||
|
endPoint: process.env.S3_ENDPOINT,
|
||||||
|
port: Number(process.env.S3_PORT),
|
||||||
|
useSSL: toBoolean(process.env.S3_USE_SSL),
|
||||||
|
accessKey: process.env.S3_ACCESS_KEY,
|
||||||
|
secretKey: process.env.S3_SECRET_KEY,
|
||||||
|
defaultBucket: process.env.S3_BUCKET,
|
||||||
|
defaultRegion: process.env.S3_REGION,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createStorageClientInstance
|
72
packages/marketplace_server/src/controllers/static/index.js
Normal file
72
packages/marketplace_server/src/controllers/static/index.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import path from "path"
|
||||||
|
import fs from "fs"
|
||||||
|
import LiveDirectory from "live-directory"
|
||||||
|
|
||||||
|
function serveStaticFiles(req, res, live_dir) {
|
||||||
|
const path = req.path.replace("/static", "")
|
||||||
|
|
||||||
|
const asset = live_dir.get(path)
|
||||||
|
|
||||||
|
if (!asset) {
|
||||||
|
return res.status(404).send("Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.cached) {
|
||||||
|
return res.send(asset.content)
|
||||||
|
} else {
|
||||||
|
const readable = asset.stream()
|
||||||
|
|
||||||
|
return readable.pipe(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serveRemoteStatic(req, res) {
|
||||||
|
global.storage.getObject(process.env.S3_BUCKET, req.path, (err, dataStream) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err)
|
||||||
|
return res.status(404).send("Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// on end of stream, dispath res.on("finish")
|
||||||
|
dataStream.on("end", () => {
|
||||||
|
res.emit("finish")
|
||||||
|
return res.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
return dataStream.pipe(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncFolder(dir) {
|
||||||
|
const files = await fs.promises.readdir(dir)
|
||||||
|
|
||||||
|
for await (const file of files) {
|
||||||
|
const filePath = path.resolve(dir, file)
|
||||||
|
|
||||||
|
const stat = fs.statSync(filePath)
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
await syncFolder(filePath)
|
||||||
|
} else {
|
||||||
|
const fileContent = await fs.promises.readFile(filePath)
|
||||||
|
|
||||||
|
await global.storage.putObject(process.env.S3_BUCKET, filePath.replace(process.cwd(), ""), fileContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (router) => {
|
||||||
|
const StaticDirectory = new LiveDirectory(path.resolve(process.cwd(), "static"), {
|
||||||
|
static: true
|
||||||
|
})
|
||||||
|
|
||||||
|
//const static_dir = path.resolve(process.cwd(), "static")
|
||||||
|
//syncFolder(static_dir)
|
||||||
|
|
||||||
|
router.get("*", (req, res) => serveRemoteStatic(req, res, StaticDirectory))
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: "/static/",
|
||||||
|
router,
|
||||||
|
}
|
||||||
|
}
|
14
packages/marketplace_server/src/controllers/widgets/index.js
Normal file
14
packages/marketplace_server/src/controllers/widgets/index.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import path from "path"
|
||||||
|
import createRoutesFromDirectory from "@utils/createRoutesFromDirectory"
|
||||||
|
|
||||||
|
export default (router) => {
|
||||||
|
// create a file based router
|
||||||
|
const routesPath = path.resolve(__dirname, "routes")
|
||||||
|
|
||||||
|
router = createRoutesFromDirectory("routes", routesPath, router)
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: "/widgets",
|
||||||
|
router,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,168 @@
|
|||||||
|
import { Widget } from "@models"
|
||||||
|
|
||||||
|
import { transform } from "sucrase"
|
||||||
|
import UglifyJS from "uglify-js"
|
||||||
|
|
||||||
|
import resolveUrl from "@utils/resolveUrl"
|
||||||
|
import replaceImportsWithRemoteURL from "@utils/replaceImportsWithRemoteURL"
|
||||||
|
|
||||||
|
async function compileWidgetCode(widgetCode, manifest, rootURL) {
|
||||||
|
if (!widgetCode) {
|
||||||
|
throw new Error("Widget code not defined.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifest) {
|
||||||
|
throw new Error("Manifest not defined.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rootURL) {
|
||||||
|
throw new Error("Root URL not defined.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderComponentName = null
|
||||||
|
let cssFiles = []
|
||||||
|
|
||||||
|
// inject react with cdn
|
||||||
|
widgetCode = `import React from "https://cdn.skypack.dev/react@17?dts" \n${widgetCode}`
|
||||||
|
|
||||||
|
widgetCode = await replaceImportsWithRemoteURL(widgetCode, resolveUrl(rootURL, manifest.entryFile))
|
||||||
|
|
||||||
|
// remove css imports and append to manifest (Only its used in the entry file)
|
||||||
|
widgetCode = widgetCode.replace(/import\s+["'](.*)\.css["']/g, (match, p1) => {
|
||||||
|
cssFiles.push(resolveUrl(rootURL, `${p1}.css`))
|
||||||
|
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
|
||||||
|
// transform jsx to js
|
||||||
|
widgetCode = await transform(widgetCode, {
|
||||||
|
transforms: ["jsx"],
|
||||||
|
//jsxRuntime: "automatic",
|
||||||
|
//production: true,
|
||||||
|
}).code
|
||||||
|
|
||||||
|
// search export default and get the name of the function/const/class
|
||||||
|
const exportDefaultRegex = /export\s+default\s+(?:function|const|class)\s+([a-zA-Z0-9]+)/g
|
||||||
|
|
||||||
|
const exportDefaultMatch = exportDefaultRegex.exec(widgetCode)
|
||||||
|
|
||||||
|
if (exportDefaultMatch) {
|
||||||
|
renderComponentName = exportDefaultMatch[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove export default keywords
|
||||||
|
widgetCode = widgetCode.replace("export default", "")
|
||||||
|
|
||||||
|
|
||||||
|
let manifestProcessed = {
|
||||||
|
...manifest,
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestProcessed.cssFiles = cssFiles
|
||||||
|
manifestProcessed.entryFile = resolveUrl(rootURL, manifest.entryFile)
|
||||||
|
manifestProcessed.iconUrl = resolveUrl(rootURL, manifest.iconUrl)
|
||||||
|
|
||||||
|
let result = `
|
||||||
|
${widgetCode}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
manifest: ${JSON.stringify(manifestProcessed)},
|
||||||
|
renderComponent: ${renderComponentName},
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// minify code
|
||||||
|
result = UglifyJS.minify(result, {
|
||||||
|
compress: true,
|
||||||
|
mangle: true,
|
||||||
|
}).code
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const widgetId = req.params.widgetId
|
||||||
|
|
||||||
|
const useCache = req.query["use-cache"] ? toBoolean(req.query["use-cache"]) : true
|
||||||
|
|
||||||
|
//console.log(`📦 Getting widget code [${widgetId}], using cache ${useCache}`)
|
||||||
|
|
||||||
|
// try to find Widget
|
||||||
|
const widget = await Widget.findOne({
|
||||||
|
_id: widgetId,
|
||||||
|
}).catch(() => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "Widget not found.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.manifest.entryFile) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "Widget entry file not defined",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let widgetCode = null
|
||||||
|
|
||||||
|
// get origin from request url
|
||||||
|
const origin = req.protocol + "://" + req.get("host")
|
||||||
|
const remotePath = `/static/${widgetId}/src`
|
||||||
|
|
||||||
|
const remoteEntyFilePath = resolveUrl(remotePath, widget.manifest.entryFile)
|
||||||
|
|
||||||
|
if (useCache) {
|
||||||
|
widgetCode = await global.redis.get(`widget:${widgetId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widgetCode) {
|
||||||
|
try {
|
||||||
|
widgetCode = await new Promise(async (resolve, reject) => {
|
||||||
|
await global.storage.getObject(process.env.S3_BUCKET, remoteEntyFilePath, (error, dataStream) => {
|
||||||
|
if (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = ""
|
||||||
|
|
||||||
|
dataStream.on("data", (chunk) => {
|
||||||
|
data += chunk
|
||||||
|
})
|
||||||
|
|
||||||
|
dataStream.on("end", () => {
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({
|
||||||
|
error: error.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("🔌 Compiling widget code...")
|
||||||
|
|
||||||
|
widgetCode = await compileWidgetCode(widgetCode, widget.manifest, resolveUrl(origin, remotePath))
|
||||||
|
|
||||||
|
await global.redis.set(`widget:${widgetId}`, widgetCode)
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({
|
||||||
|
message: "Unable to transform widget code.",
|
||||||
|
error: error.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widgetCode) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "Widget not found.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader("Content-Type", "application/javascript")
|
||||||
|
return res.status(200).send(widgetCode)
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Widget } from "@models"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const widget_id = req.params.widgetId
|
||||||
|
|
||||||
|
const widget = await Widget.findOne({
|
||||||
|
_id: widget_id,
|
||||||
|
}).catch(() => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "Widget not found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
...widget.manifest,
|
||||||
|
user_id: widget.user_id,
|
||||||
|
_id: widget_id,
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Widget } from "@models"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
let { limit = 20, offset = 0, keywords } = req.query
|
||||||
|
|
||||||
|
keywords = JSON.parse(keywords ?? "{}")
|
||||||
|
|
||||||
|
// remove empty keywords
|
||||||
|
Object.keys(keywords).forEach((key) => {
|
||||||
|
if (keywords[key] === "") {
|
||||||
|
delete keywords[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("Searching with keywords:", keywords)
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
public: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// inclide keywords for search in manifest
|
||||||
|
Object.keys(keywords).forEach((key) => {
|
||||||
|
query[`manifest.${key}`] = {
|
||||||
|
$regex: keywords[key],
|
||||||
|
$options: "i",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let widgets = await Widget.find(query)
|
||||||
|
.limit(Number(limit))
|
||||||
|
.skip(Number(offset))
|
||||||
|
|
||||||
|
widgets = widgets.map((widget) => {
|
||||||
|
widget.manifest._id = widget._id
|
||||||
|
widget.manifest.user_id = widget.user_id
|
||||||
|
|
||||||
|
return widget
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json(widgets)
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import { Widget } from "@models"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { user_id } = req.params
|
||||||
|
const { limit = 20, offset = 0 } = req.query
|
||||||
|
|
||||||
|
const widgets = await Widget.find({
|
||||||
|
user_id,
|
||||||
|
public: true,
|
||||||
|
})
|
||||||
|
.limit(Number(limit))
|
||||||
|
.skip(Number(offset))
|
||||||
|
|
||||||
|
return res.json(widgets)
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
export default (req, res) => {
|
||||||
|
return res.status(200).json({
|
||||||
|
test: "Testing endpoints by route files"
|
||||||
|
})
|
||||||
|
}
|
56
packages/marketplace_server/src/index.js
Normal file
56
packages/marketplace_server/src/index.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
require("dotenv").config()
|
||||||
|
import { webcrypto as crypto } from "crypto"
|
||||||
|
import path from "path"
|
||||||
|
import { registerBaseAliases } from "linebridge/dist/server"
|
||||||
|
|
||||||
|
registerBaseAliases(undefined, {
|
||||||
|
"@services": path.resolve(__dirname, "services"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// patches
|
||||||
|
const { Buffer } = require("buffer")
|
||||||
|
|
||||||
|
global.b64Decode = (data) => {
|
||||||
|
return Buffer.from(data, "base64").toString("utf-8")
|
||||||
|
}
|
||||||
|
global.b64Encode = (data) => {
|
||||||
|
return Buffer.from(data, "utf-8").toString("base64")
|
||||||
|
}
|
||||||
|
|
||||||
|
global.nanoid = (t = 21) => crypto.getRandomValues(new Uint8Array(t)).reduce(((t, e) => t += (e &= 63) < 36 ? e.toString(36) : e < 62 ? (e - 26).toString(36).toUpperCase() : e > 62 ? "-" : "_"), "");
|
||||||
|
|
||||||
|
Array.prototype.updateFromObjectKeys = function (obj) {
|
||||||
|
this.forEach((value, index) => {
|
||||||
|
if (obj[value] !== undefined) {
|
||||||
|
this[index] = obj[value]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
global.toBoolean = (value) => {
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.toLowerCase() === "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
global.isProduction = process.env.NODE_ENV === "production"
|
||||||
|
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const mainAPI = new API()
|
||||||
|
|
||||||
|
await mainAPI.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`🆘 [FATAL ERROR] >`, error)
|
||||||
|
})
|
19
packages/marketplace_server/src/models/index.js
Executable file
19
packages/marketplace_server/src/models/index.js
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
import mongoose, { Schema } from "mongoose"
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
function generateModels() {
|
||||||
|
let models = {}
|
||||||
|
|
||||||
|
const dirs = fs.readdirSync(__dirname).filter(file => file !== "index.js")
|
||||||
|
|
||||||
|
dirs.forEach((file) => {
|
||||||
|
const model = require(path.join(__dirname, file)).default
|
||||||
|
|
||||||
|
models[model.name] = mongoose.model(model.name, new Schema(model.schema), model.collection)
|
||||||
|
})
|
||||||
|
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = generateModels()
|
24
packages/marketplace_server/src/models/widget/index.js
Normal file
24
packages/marketplace_server/src/models/widget/index.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
name: "Widget",
|
||||||
|
collection: "widgets",
|
||||||
|
schema: {
|
||||||
|
manifest: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
public: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import cors from "cors"
|
||||||
|
|
||||||
|
export default cors({
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"],
|
||||||
|
preflightContinue: false,
|
||||||
|
optionsSuccessStatus: 204,
|
||||||
|
})
|
@ -0,0 +1,14 @@
|
|||||||
|
export default (req, res, next) => {
|
||||||
|
const startHrTime = process.hrtime()
|
||||||
|
|
||||||
|
res.on("finish", () => {
|
||||||
|
const elapsedHrTime = process.hrtime(startHrTime)
|
||||||
|
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
|
||||||
|
|
||||||
|
res._responseTimeMs = elapsedTimeInMs
|
||||||
|
|
||||||
|
console.log(`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${req.url} ${elapsedTimeInMs}ms`)
|
||||||
|
})
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
function createRoutesFromDirectory(startFrom, directoryPath, router) {
|
||||||
|
const files = fs.readdirSync(directoryPath)
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const filePath = `${directoryPath}/${file}`
|
||||||
|
|
||||||
|
const stat = fs.statSync(filePath)
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
createRoutesFromDirectory(startFrom, filePath, router)
|
||||||
|
} else if (file.endsWith(".js")) {
|
||||||
|
let splitedFilePath = filePath.split("/")
|
||||||
|
|
||||||
|
// slice the startFrom path
|
||||||
|
splitedFilePath = splitedFilePath.slice(splitedFilePath.indexOf(startFrom) + 1)
|
||||||
|
|
||||||
|
const method = splitedFilePath[0]
|
||||||
|
|
||||||
|
let route = splitedFilePath.slice(1, splitedFilePath.length).join("/")
|
||||||
|
|
||||||
|
route = route.replace(".jsx", "")
|
||||||
|
route = route.replace(".js", "")
|
||||||
|
route = route.replace(".ts", "")
|
||||||
|
route = route.replace(".tsx", "")
|
||||||
|
|
||||||
|
if (route === "index") {
|
||||||
|
route = "/"
|
||||||
|
} else {
|
||||||
|
route = `/${route}`
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler = require(filePath)
|
||||||
|
|
||||||
|
handler = handler.default || handler
|
||||||
|
|
||||||
|
router[method](route, handler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createRoutesFromDirectory
|
@ -0,0 +1,18 @@
|
|||||||
|
import resolveUrl from "@utils/resolveUrl"
|
||||||
|
|
||||||
|
export default (code, rootURL) => {
|
||||||
|
const importRegex = /import\s+(?:(?:([\w*\s{},]*)\s+from\s+)?["']([^"']*)["']|["']([^"']*)["'])/g
|
||||||
|
|
||||||
|
// replaces all imports with absolute paths
|
||||||
|
const absoluteImportCode = code.replace(importRegex, (match, p1, p2) => {
|
||||||
|
let resolved = resolveUrl(rootURL, p2)
|
||||||
|
|
||||||
|
if (!p1) {
|
||||||
|
return `import "${resolved}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `import ${p1} from "${resolved}"`
|
||||||
|
})
|
||||||
|
|
||||||
|
return absoluteImportCode
|
||||||
|
}
|
11
packages/marketplace_server/src/utils/resolveUrl/index.js
Normal file
11
packages/marketplace_server/src/utils/resolveUrl/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export default (from, to) => {
|
||||||
|
const resolvedUrl = new URL(to, new URL(from, "resolve://"))
|
||||||
|
|
||||||
|
if (resolvedUrl.protocol === "resolve:") {
|
||||||
|
const { pathname, search, hash } = resolvedUrl
|
||||||
|
|
||||||
|
return pathname + search + hash
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedUrl.toString()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user