diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5bc0e65..d7df89c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,3 @@ { - "recommendations": [ - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint" - ] + "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] } diff --git a/apps/blueprint-image-function/function.package.json b/apps/blueprint-image-function/function.package.json new file mode 100644 index 0000000..ae66114 --- /dev/null +++ b/apps/blueprint-image-function/function.package.json @@ -0,0 +1,20 @@ +{ + "name": "blueprint-image-function", + "version": "0.0.1", + "scripts": { + "postinstall": "yarn prisma generate" + }, + "dependencies": { + "puppeteer": "7.1.0", + "tslib": "2.1.0", + "sharp": "0.27.2", + "prisma": "2.18.0", + "@prisma/client": "2.18.0", + "@google-cloud/pubsub": "2.9.0", + "@google-cloud/secret-manager": "3.4.0", + "@google-cloud/storage": "5.7.4", + "cookie": "0.4.1", + "pako": "1.0.11", + "bcrypt": "5.0.0" + } +} diff --git a/apps/blueprint-image-function/src/function-handler.ts b/apps/blueprint-image-function/src/function-handler.ts new file mode 100644 index 0000000..68c282d --- /dev/null +++ b/apps/blueprint-image-function/src/function-handler.ts @@ -0,0 +1,136 @@ +import { + getBlueprintById, + getBlueprintStringByHash, + hasBlueprintImage, + saveBlueprintImage, + init, +} from "@factorio-sites/database"; +import { jsonReplaceErrors } from "@factorio-sites/node-utils"; +import { optimise } from "./image-optimiser"; +import { renderImage } from "./image-renderer"; + +// {"blueprintId":"ee9b98eb-313a-4401-8aee-d6e970b76aad"} +// ^ image_hash: 6f78c0a93c20fe99076e8defe4e396923f42753b + +/** express req interface for http-triggered function */ +interface Req { + query: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body: any; +} +/** express res interface for http-triggered function */ +interface Res { + status(status: number): Res; + send(body: string): void; +} +/** message body for pubsub triggered function */ +type Message = Record; +/** context for pubsub triggered function */ +interface Context { + eventId: string; + eventType: string; + timestamp: string; + resource: { service: string; name: string }; +} +type Handler = (req: Req, res: Res) => void; +type PubSubHandler = ( + message: Message, + context: Context, + callback: (error?: string) => void +) => void; + +export const functionHttpHandler: Handler = async (req, res) => { + try { + const blueprintId = req.body.blueprintId; + + if (!blueprintId) { + return res.status(400).send("No blueprintId in body"); + } + + console.log("generating image for", blueprintId); + + await init(); + const blueprint = await getBlueprintById(blueprintId); + + if (!blueprint) { + return res.status(400).send("Blueprint not found"); + } + + if (await hasBlueprintImage(blueprint.image_hash, "300")) { + return res.status(200).send("Image already exists"); + } + + const blueprint_string = await getBlueprintStringByHash(blueprint.blueprint_hash); + if (!blueprint_string) { + return res.status(400).send("Blueprint string not found"); + } + + const image = await renderImage(blueprint_string); + console.log("Image generated"); + + // Make thumbnail, max size 300px + const min_image = await optimise(image, 300); + + await saveBlueprintImage(blueprint.image_hash, min_image, "300"); + + res.status(200).send("Done"); + } catch (reason) { + res.status(500).send(`Error rendering image ${reason.stack || reason}`); + } +}; + +export const functionPubSubHandler: PubSubHandler = async (message, _context, callback) => { + const error = (message: string, data: Record = {}) => { + console.error(JSON.stringify({ message, ...data }, jsonReplaceErrors)); + callback(message); + }; + const log = (message: string, data: Record = {}) => { + console.log(JSON.stringify({ message, ...data })); + }; + + try { + const data = message.data + ? JSON.parse(Buffer.from(message.data, "base64").toString()) + : message; + + const blueprintId = data.blueprintId; + + if (!blueprintId) { + return error("No blueprintId in body"); + } + + log(`generating image for ${blueprintId}`); + + await init(); + const blueprint = await getBlueprintById(blueprintId); + + if (!blueprint) { + return error("Blueprint not found"); + } + + if (await hasBlueprintImage(blueprint.image_hash, "300")) { + log("Image already exists"); + return callback(); + } + + const blueprint_string = await getBlueprintStringByHash(blueprint.blueprint_hash); + if (!blueprint_string) { + return error("Blueprint string not found"); + } + + const image = await renderImage(blueprint_string); + log("Image generated"); + + // Make thumbnail, max size 300px + const min_image = await optimise(image, 300); + + await saveBlueprintImage(blueprint.image_hash, min_image, "300"); + log(`Saved image with image hash ${blueprint.image_hash}`); + + await saveBlueprintImage(blueprint.image_hash, image, "original"); + + callback(); + } catch (reason) { + error(`Error rendering image ${reason}`, { error: reason }); + } +}; diff --git a/apps/blueprint-image-function/src/image-optimiser.ts b/apps/blueprint-image-function/src/image-optimiser.ts index 1c804e6..d4123d7 100644 --- a/apps/blueprint-image-function/src/image-optimiser.ts +++ b/apps/blueprint-image-function/src/image-optimiser.ts @@ -1,50 +1,27 @@ import * as sharp from "sharp"; -const RESIZE_ENABLED = false; - // const calculateImageSizeMod = (pixels: number) => // Math.min(Math.max((-pixels + 500) / 20500 + 1, 0.3), 1); +// const calculateImageSizeMod = (pixels: number) => +// Math.min(Math.max((-pixels + 3000) / 33000 + 1, 0.3), 1); +// const mod = calculateImageSizeMod(Math.max(meta.width, meta.height)); -const calculateImageSizeMod = (pixels: number) => - Math.min(Math.max((-pixels + 3000) / 33000 + 1, 0.3), 1); +export const optimise = async (image: Buffer, max_dimention = 5000): Promise => { + const sharp_image = await sharp(image) + .resize({ + width: max_dimention, + height: max_dimention, + fit: sharp.fit.inside, + }) + .webp({ lossless: true }) + .toBuffer(); -export const optimise = async (image: Buffer): Promise => { - let sharp_image = sharp(image); - if (RESIZE_ENABLED) { - const MAX_IMAGE_DIMENTION = 5000; - sharp_image = await sharp_image - .metadata() - .then((meta) => { - if ( - meta.width && - meta.height && - (meta.width > MAX_IMAGE_DIMENTION || meta.height > MAX_IMAGE_DIMENTION) - ) { - const mod = calculateImageSizeMod(Math.max(meta.width, meta.height)); - console.log({ - width: meta.width, - height: meta.height, - mod, - size_mb: image.byteLength / 1024_000, - }); - return sharp_image.resize({ - width: Math.round(meta.width * mod), - height: Math.round(meta.height * mod), - }); - } - return sharp_image; - }) - .then((image) => image.webp({ lossless: true })); - } else { - sharp_image = sharp_image.webp({ lossless: true }); - } + console.log( + JSON.stringify({ + input_size: `${Math.round(image.byteLength / 1024) / 1000}mb`, + output_size: `${Math.round(sharp_image.byteLength / 1024) / 1000}mb`, + }) + ); - const min_image = await sharp_image.toBuffer(); - - console.log({ - input_size_mb: image.byteLength / 1024_000, - output_size_mb: min_image.byteLength / 1024_000, - }); - - return min_image; + return sharp_image; }; diff --git a/apps/blueprint-image-function/src/image-renderer.ts b/apps/blueprint-image-function/src/image-renderer.ts index 7588eaa..4020e6e 100644 --- a/apps/blueprint-image-function/src/image-renderer.ts +++ b/apps/blueprint-image-function/src/image-renderer.ts @@ -4,11 +4,11 @@ import * as Puppeteer from "puppeteer"; let BROWSER: Puppeteer.Browser; let PAGE: Puppeteer.Page; -async function getPage() { +async function getPage(headless: boolean) { if (PAGE) return PAGE; const _browser = await Puppeteer.launch({ - headless: false, + headless, args: ["--no-sandbox"], }); @@ -24,9 +24,9 @@ async function getPage() { return _page; } -export async function renderImage(blueprint_string: string) { +export async function renderImage(blueprint_string: string, options?: { headless: boolean }) { const tl = timeLogger("localFbeRenderer"); - const page = await getPage(); + const page = await getPage(options?.headless ?? true); tl("Page loaded"); diff --git a/apps/blueprint-image-function/src/local-file-upload.ts b/apps/blueprint-image-function/src/local-file-upload.ts deleted file mode 100644 index b6ccfcd..0000000 --- a/apps/blueprint-image-function/src/local-file-upload.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { promisify } from "util"; -import { - hasBlueprintImage, - getBlueprintByImageHash, - saveBlueprintImage, -} from "@factorio-sites/database"; -import { optimise } from "./image-optimiser"; - -if (!process.env.DIR) throw Error("no 'DIR' environment variable"); - -const fsReadFile = promisify(fs.readFile); -const fsUnlink = promisify(fs.unlink); -const FILE_DIR = path.normalize(process.env.DIR); - -const uploadFile = async (image_path: string) => { - const image_hash = path.basename(image_path, ".png"); - - if (!(await getBlueprintByImageHash(image_hash))) { - console.log(`Image ${image_hash} has no database record, skipping...`); - return; - } - if (image_hash.includes("(")) { - console.log(`Image ${image_hash} is a duplicate, deleting...`); - await fsUnlink(image_path); - return; - } - if (await hasBlueprintImage(image_hash)) { - console.log(`Image ${image_hash} already exists, deleting...`); - await fsUnlink(image_path); - return; - } - - console.log(`Processing ${image_hash}...`); - const image = await fsReadFile(image_path); - const min_image = await optimise(image); - - console.log(`Image ${image_hash} processed, uploading...`); - await saveBlueprintImage(image_hash, min_image); - await fsUnlink(image_path); -}; - -export async function uploadLocalFiles() { - // console.log(`Reading directory`, FILE_DIR); - // const files = await fsReaddir(FILE_DIR); - // for (let i = 0; i < files.length; i++) { - // if (fs.statSync(path.join(FILE_DIR, files[i])).isDirectory()) continue; - // await uploadFile(path.join(FILE_DIR, files[i])); - // } - console.log(`Watching directory`, FILE_DIR); - const work_done_buffeer: string[] = []; - const work_buffer: string[] = []; - fs.watch(FILE_DIR, (type, file) => { - if (type === "change" && file && file.endsWith(".png")) { - const file_path = path.join(FILE_DIR, file); - if (work_buffer.includes(file_path) || work_done_buffeer.includes(file_path)) { - return; - } - work_buffer.push(file_path); - } - }); - - let working = false; - const doWork = async () => { - if (working || !work_buffer.length) return setTimeout(doWork, 1000); - working = true; - const file_path = work_buffer.shift(); - if (file_path) { - await uploadFile(file_path); - work_done_buffeer.push(file_path); - } - working = false; - doWork(); - }; - doWork(); -} diff --git a/apps/blueprint-image-function/src/local-test.ts b/apps/blueprint-image-function/src/local-test.ts new file mode 100644 index 0000000..831e6c8 --- /dev/null +++ b/apps/blueprint-image-function/src/local-test.ts @@ -0,0 +1,40 @@ +import { + getBlueprintById, + getBlueprintStringByHash, + hasBlueprintImage, + init, + saveBlueprintImage, +} from "@factorio-sites/database"; +import { optimise } from "./image-optimiser"; +import { renderImage } from "./image-renderer"; + +export async function local_test(blueprint_id: string) { + await init(); + const blueprint = await getBlueprintById(blueprint_id); + + if (!blueprint) { + return console.log("Blueprint not found"); + } + + if (await hasBlueprintImage(blueprint.image_hash, "300")) { + // return console.log("Image already exists"); + } + + const blueprint_string = await getBlueprintStringByHash(blueprint.blueprint_hash); + if (!blueprint_string) { + return console.log("Blueprint string not found"); + } + + const image = await renderImage(blueprint_string, { headless: false }); + console.log("Image generated"); + + // Make thumbnail, max size 300px + const min_image = await optimise(image, 300); + + await saveBlueprintImage(blueprint.image_hash, min_image, "300"); + console.log(`Saved image with image hash ${blueprint.image_hash}`); + + await saveBlueprintImage(blueprint.image_hash, image, "original"); + + console.log("done"); +} diff --git a/apps/blueprint-image-function/src/main.ts b/apps/blueprint-image-function/src/main.ts index fc30f3d..de7373b 100644 --- a/apps/blueprint-image-function/src/main.ts +++ b/apps/blueprint-image-function/src/main.ts @@ -1,11 +1,11 @@ -import { subscribeToPubSub } from "./pubsub-render"; +import { functionHttpHandler, functionPubSubHandler } from "./function-handler"; +// import { local_test } from "./local-test"; -subscribeToPubSub().catch((reason) => console.error("Fatal error:", reason)); -// uploadLocalFiles().catch((reason) => console.error("Fatal error:", reason)); +// import { subscribeToPubSub } from "./pubsub-render"; +// subscribeToPubSub().catch((reason) => console.error("Fatal error:", reason)); // rePublishAllBlueprints().catch((reason) => console.error("Fatal error:", reason)); -// image hash = a99525f97c26c7242ecdd96679043b1a5e65dd0c -// SELECT * FROM BlueprintBook WHERE blueprint_ids CONTAINS Key(Blueprint, 4532736400293888) -// bp = Key(Blueprint, 4532736400293888) -// book = Key(BlueprintBook, 5034207050989568) -// page = 6225886932107264 +exports.renderImageHttp = functionHttpHandler; +exports.renderImagePubSub = functionPubSubHandler; + +// local_test("8737437e-f15b-459c-8c1d-d0074f3a89ca"); diff --git a/apps/blueprint-image-function/src/pubsub-render.ts b/apps/blueprint-image-function/src/pubsub-render.ts index e4e4ad2..6a8d20c 100644 --- a/apps/blueprint-image-function/src/pubsub-render.ts +++ b/apps/blueprint-image-function/src/pubsub-render.ts @@ -35,11 +35,12 @@ export async function subscribeToPubSub() { }; try { - const data = JSON.parse(message.data.toString()); - if (!data.blueprintId) return ack("blueprintId not found in message body", false); + if (!message.attributes.blueprintId) { + return ack("blueprintId not found in message body", false); + } console.log("------------------------------------------------"); - console.log("[pubsub] generating image for", data.blueprintId); - const blueprint = await getBlueprintById(data.blueprintId); + console.log("[pubsub] generating image for", message.attributes.blueprintId); + const blueprint = await getBlueprintById(message.attributes.blueprintId); if (!blueprint) return ack("Blueprint not found", false); if (await hasBlueprintImage(blueprint.image_hash)) { @@ -58,7 +59,7 @@ export async function subscribeToPubSub() { return ack("[pubsub] image saved", true); } catch (reason) { - return ack(`[pubsub:error] ${reason}`, false); + return ack(`[pubsub:error] ${reason.stack || reason}`, false); } }; diff --git a/apps/blueprint-image-function/src/republish-pubsub.ts b/apps/blueprint-image-function/src/republish-pubsub.ts index ac40b30..1efdad6 100644 --- a/apps/blueprint-image-function/src/republish-pubsub.ts +++ b/apps/blueprint-image-function/src/republish-pubsub.ts @@ -1,20 +1,22 @@ -import { getBlueprintImageRequestTopic, getPaginatedBlueprints } from "@factorio-sites/database"; +// import { getBlueprintImageRequestTopic, getPaginatedBlueprints } from "@factorio-sites/database"; -export async function rePublishAllBlueprints() { - const topic = getBlueprintImageRequestTopic(); - const fetchPage = async (page = 1) => { - const blueprints = await getPaginatedBlueprints(page); - if (blueprints.length === 0) { - return console.log("No more blueprints found"); - } - console.log(`Publishing page ${page} with ${blueprints.length} blueprints`); +// export async function rePublishAllBlueprints() { +// const topic = getBlueprintImageRequestTopic(); +// const fetchPage = async (page = 1) => { +// const blueprints = await getPaginatedBlueprints(page); +// if (blueprints.length === 0) { +// return console.log("No more blueprints found"); +// } +// console.log(`Publishing page ${page} with ${blueprints.length} blueprints`); - await Promise.all( - blueprints.map((blueprint) => { - return topic.publishJSON({ blueprintId: blueprint.id }); - }) - ); - fetchPage(page + 1); - }; - await fetchPage(); -} +// await Promise.all( +// blueprints.map((blueprint) => { +// return topic.publishJSON({ blueprintId: blueprint.id }); +// }) +// ); +// fetchPage(page + 1); +// }; +// await fetchPage(); +// } + +export {}; diff --git a/apps/blueprint-image-function/tsconfig.app.json b/apps/blueprint-image-function/tsconfig.app.json index bb717c5..d98e917 100644 --- a/apps/blueprint-image-function/tsconfig.app.json +++ b/apps/blueprint-image-function/tsconfig.app.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", + "target": "ES2019", "types": ["node"] }, "exclude": ["**/*.spec.ts"], diff --git a/apps/blueprints/next.config.js b/apps/blueprints/next.config.js index 25e5713..581ef5c 100644 --- a/apps/blueprints/next.config.js +++ b/apps/blueprints/next.config.js @@ -1,14 +1,17 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); module.exports = { poweredByHeader: false, reactStrictMode: true, + images: { + domains: ["storage.googleapis.com"], + }, webpack(config, options) { const { dev, isServer } = options; // Do not run type checking twice: if (dev && isServer) { + const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); config.plugins.push(new ForkTsCheckerWebpackPlugin()); } diff --git a/apps/blueprints/prisma/migrations/20210304215502_update_field_types/migration.sql b/apps/blueprints/prisma/migrations/20210304215502_update_field_types/migration.sql deleted file mode 100644 index 59a40ab..0000000 --- a/apps/blueprints/prisma/migrations/20210304215502_update_field_types/migration.sql +++ /dev/null @@ -1,50 +0,0 @@ -/* - Warnings: - - - The migration will add a unique constraint covering the columns `[blueprint_hash]` on the table `blueprint_book`. If there are existing duplicate values, the migration will fail. - - The migration will add a unique constraint covering the columns `[factorioprints_id]` on the table `blueprint_page`. If there are existing duplicate values, the migration will fail. - - The migration will add a unique constraint covering the columns `[session_token]` on the table `session`. If there are existing duplicate values, the migration will fail. - -*/ --- AlterTable -ALTER TABLE "blueprint" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP, -ALTER COLUMN "created_at" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "updated_at" SET DATA TYPE TIMESTAMP(3); - --- AlterTable -ALTER TABLE "blueprint_book" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP, -ALTER COLUMN "created_at" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "updated_at" SET DATA TYPE TIMESTAMP(3); - --- AlterTable -ALTER TABLE "blueprint_page" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP, -ALTER COLUMN "created_at" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "updated_at" SET DATA TYPE TIMESTAMP(3); - --- AlterTable -ALTER TABLE "session" ALTER COLUMN "last_used" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP, -ALTER COLUMN "created_at" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "updated_at" SET DATA TYPE TIMESTAMP(3); - --- AlterTable -ALTER TABLE "user" ALTER COLUMN "password_reset_at" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "last_password_change" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "last_login_at" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP, -ALTER COLUMN "created_at" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "updated_at" SET DATA TYPE TIMESTAMP(3); - --- AlterTable -ALTER TABLE "user_favorites" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP, -ALTER COLUMN "created_at" SET DATA TYPE TIMESTAMP(3), -ALTER COLUMN "updated_at" SET DATA TYPE TIMESTAMP(3); - --- CreateIndex -CREATE UNIQUE INDEX "blueprint_book.blueprint_hash_unique" ON "blueprint_book"("blueprint_hash"); - --- CreateIndex -CREATE UNIQUE INDEX "blueprint_page.factorioprints_id_unique" ON "blueprint_page"("factorioprints_id"); - --- CreateIndex -CREATE UNIQUE INDEX "session.session_token_unique" ON "session"("session_token"); diff --git a/apps/blueprints/prisma/migrations/20210305231036_initial_setup/migration.sql b/apps/blueprints/prisma/migrations/20210305231036_initial_setup/migration.sql new file mode 100644 index 0000000..6c08bdf --- /dev/null +++ b/apps/blueprints/prisma/migrations/20210305231036_initial_setup/migration.sql @@ -0,0 +1,171 @@ +-- CreateEnum +CREATE TYPE "enum_user_role" AS ENUM ('user', 'moderator', 'admin'); + +-- CreateTable +CREATE TABLE "blueprint" ( + "id" UUID NOT NULL, + "label" VARCHAR(255), + "description" TEXT, + "game_version" VARCHAR(255), + "blueprint_hash" VARCHAR(40) NOT NULL, + "image_hash" VARCHAR(40) NOT NULL, + "image_version" INTEGER NOT NULL DEFAULT 1, + "tags" VARCHAR(255)[], + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "blueprint_book" ( + "id" UUID NOT NULL, + "label" VARCHAR(255), + "description" TEXT, + "child_tree" JSON NOT NULL, + "blueprint_hash" VARCHAR(40) NOT NULL, + "is_modded" BOOLEAN NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "blueprint_page" ( + "id" UUID NOT NULL, + "user_id" UUID, + "blueprint_id" UUID, + "blueprint_book_id" UUID, + "title" VARCHAR(255) NOT NULL, + "description_markdown" TEXT, + "tags" VARCHAR(255)[], + "factorioprints_id" VARCHAR(255), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "session" ( + "id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "session_token" UUID NOT NULL, + "useragent" VARCHAR(255) NOT NULL, + "ip" VARCHAR(255) NOT NULL, + "last_used" TIMESTAMP(3) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user" ( + "id" UUID NOT NULL, + "email" VARCHAR(255), + "username" VARCHAR(255) NOT NULL, + "role" "enum_user_role" DEFAULT E'user', + "steam_id" VARCHAR(255), + "password" VARCHAR(255), + "password_reset_token" UUID, + "password_reset_at" TIMESTAMP(3), + "last_password_change" TIMESTAMP(3), + "last_login_at" TIMESTAMP(3), + "last_login_ip" VARCHAR(255) NOT NULL, + "email_validated" BOOLEAN NOT NULL DEFAULT false, + "email_validate_token" UUID, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_favorites" ( + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "user_id" UUID NOT NULL, + "blueprint_page_id" UUID NOT NULL, + + PRIMARY KEY ("user_id","blueprint_page_id") +); + +-- CreateTable +CREATE TABLE "_blueprintToblueprint_book" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "_blueprint_books" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "blueprint.blueprint_hash_unique" ON "blueprint"("blueprint_hash"); + +-- CreateIndex +CREATE UNIQUE INDEX "blueprint_book.blueprint_hash_unique" ON "blueprint_book"("blueprint_hash"); + +-- CreateIndex +CREATE UNIQUE INDEX "blueprint_page.blueprint_id_unique" ON "blueprint_page"("blueprint_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "blueprint_page.blueprint_book_id_unique" ON "blueprint_page"("blueprint_book_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "blueprint_page.factorioprints_id_unique" ON "blueprint_page"("factorioprints_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "session.session_token_unique" ON "session"("session_token"); + +-- CreateIndex +CREATE UNIQUE INDEX "user.email_unique" ON "user"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "user.username_unique" ON "user"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "user.steam_id_unique" ON "user"("steam_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "_blueprintToblueprint_book_AB_unique" ON "_blueprintToblueprint_book"("A", "B"); + +-- CreateIndex +CREATE INDEX "_blueprintToblueprint_book_B_index" ON "_blueprintToblueprint_book"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_blueprint_books_AB_unique" ON "_blueprint_books"("A", "B"); + +-- CreateIndex +CREATE INDEX "_blueprint_books_B_index" ON "_blueprint_books"("B"); + +-- AddForeignKey +ALTER TABLE "blueprint_page" ADD FOREIGN KEY ("blueprint_book_id") REFERENCES "blueprint_book"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "blueprint_page" ADD FOREIGN KEY ("blueprint_id") REFERENCES "blueprint"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "session" ADD FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_favorites" ADD FOREIGN KEY ("blueprint_page_id") REFERENCES "blueprint_page"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_favorites" ADD FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_blueprintToblueprint_book" ADD FOREIGN KEY ("A") REFERENCES "blueprint"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_blueprintToblueprint_book" ADD FOREIGN KEY ("B") REFERENCES "blueprint_book"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_blueprint_books" ADD FOREIGN KEY ("A") REFERENCES "blueprint_book"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_blueprint_books" ADD FOREIGN KEY ("B") REFERENCES "blueprint_book"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/blueprints/prisma/migrations/20210306110734_book_image_hash/migration.sql b/apps/blueprints/prisma/migrations/20210306110734_book_image_hash/migration.sql new file mode 100644 index 0000000..1d909bd --- /dev/null +++ b/apps/blueprints/prisma/migrations/20210306110734_book_image_hash/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `image_hash` to the `blueprint_page` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "blueprint_page" ADD COLUMN "image_hash" VARCHAR(40) NOT NULL; diff --git a/apps/blueprints/prisma/schema.prisma b/apps/blueprints/prisma/schema.prisma index 685a331..9196f96 100644 --- a/apps/blueprints/prisma/schema.prisma +++ b/apps/blueprints/prisma/schema.prisma @@ -14,43 +14,33 @@ enum enum_user_role { } model blueprint { - id String @id @default(uuid()) @db.Uuid - label String? @db.VarChar(255) - description String? - game_version String? @db.VarChar(255) - blueprint_hash String @unique @db.VarChar(40) - image_hash String @db.VarChar(40) - image_version Int @default(1) - tags String[] @db.VarChar(255) - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - blueprint_page blueprint_page? + id String @id @default(uuid()) @db.Uuid + label String? @db.VarChar(255) + description String? + game_version String? @db.VarChar(255) + blueprint_hash String @unique @db.VarChar(40) + image_hash String @db.VarChar(40) + image_version Int @default(1) + tags String[] @db.VarChar(255) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + blueprint_page blueprint_page? + blueprint_books blueprint_book[] } model blueprint_book { - id String @id @default(uuid()) @db.Uuid - label String? @db.VarChar(255) - description String? - child_tree Json @db.Json - blueprint_hash String @unique @db.VarChar(40) - is_modded Boolean - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - blueprint_page blueprint_page? -} - -model blueprint_book_blueprints { - blueprint_book_id String @db.Uuid - blueprint_id String @db.Uuid - - @@id([blueprint_book_id, blueprint_id]) -} - -model blueprint_book_books { - blueprint_book_1_id String @db.Uuid - blueprint_book_2_id String @db.Uuid - - @@id([blueprint_book_1_id, blueprint_book_2_id]) + id String @id @default(uuid()) @db.Uuid + label String? @db.VarChar(255) + description String? + child_tree Json @db.Json + blueprint_hash String @unique @db.VarChar(40) + is_modded Boolean + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + blueprint_page blueprint_page? + blueprints blueprint[] + blueprint_books blueprint_book[] @relation("blueprint_books") + blueprint_books_relation blueprint_book[] @relation("blueprint_books") } model blueprint_page { @@ -61,6 +51,7 @@ model blueprint_page { title String @db.VarChar(255) description_markdown String? tags String[] @db.VarChar(255) + image_hash String @db.VarChar(40) factorioprints_id String? @unique @db.VarChar(255) created_at DateTime @default(now()) updated_at DateTime @updatedAt diff --git a/apps/blueprints/prod.package.json b/apps/blueprints/prod.package.json index 901370d..b17cc35 100644 --- a/apps/blueprints/prod.package.json +++ b/apps/blueprints/prod.package.json @@ -1,21 +1,22 @@ { "dependencies": { - "@chakra-ui/react": "1.1.2", - "@emotion/react": "11.1.4", + "@chakra-ui/react": "1.3.3", + "@emotion/react": "11.1.5", "@emotion/server": "11.0.0", - "@emotion/styled": "11.0.0", - "@fbe/editor": "./fbe-editor-v1.0.0.tgz", - "@google-cloud/pubsub": "2.7.0", - "@google-cloud/secret-manager": "3.2.3", - "@google-cloud/storage": "5.7.0", - "@hookstate/core": "3.0.3", + "@emotion/styled": "11.1.5", + "@fbe/editor": "file:.yalc/@fbe/editor", + "@google-cloud/pubsub": "2.9.0", + "@google-cloud/secret-manager": "3.4.0", + "@google-cloud/storage": "5.7.4", + "@hookstate/core": "3.0.5", + "@prisma/client": "2.18.0", "bbcode-to-react": "0.2.9", "bcrypt": "5.0.0", "cookie": "0.4.1", "document-register-element": "1.14.10", "formik": "2.2.6", - "framer-motion": "3.1.4", - "next": "10.0.5", + "framer-motion": "3.3.0", + "next": "10.0.7", "nprogress": "0.2.0", "openid": "2.0.7", "pako": "1.0.11", @@ -23,9 +24,10 @@ "phin": "3.5.1", "react": "17.0.1", "react-dom": "17.0.1", - "react-icons": "4.1.0", + "react-icons": "4.2.0", "react-map-interaction": "2.0.0", "react-markdown": "5.0.3", - "sequelize": "6.3.5" + "react-multi-select-component": "3.1.3", + "fork-ts-checker-webpack-plugin": "6.1.1" } } diff --git a/apps/blueprints/src/components/BlueprintLink.tsx b/apps/blueprints/src/components/BlueprintLink.tsx index 9121044..fc222e9 100644 --- a/apps/blueprints/src/components/BlueprintLink.tsx +++ b/apps/blueprints/src/components/BlueprintLink.tsx @@ -1,20 +1,45 @@ import Link from "next/link"; +import Image from "next/image"; import { css } from "@emotion/react"; -import { BlueprintPage } from "@factorio-sites/database"; +import { BlueprintPage } from "@factorio-sites/types"; import { Box, Text } from "@chakra-ui/react"; import { MdFavorite } from "react-icons/md"; +import { useState } from "react"; const linkStyles = css` - width: 100%; - margin: 5px 0; + margin: 5px 10px 5px 0; + background: #353535; + + .block { + display: flex; + justify-content: space-between; + } + + .image { + position: relative; + width: 200px; + height: 200px; + } + + .details { + display: flex; + } + a { display: block; padding: 5px; color: #fff; } + &:hover { cursor: pointer; - background: #ccc; + background: #4c4c4c; + } + + &.tile { + .block { + flex-direction: column; + } } `; @@ -26,26 +51,61 @@ const formatDate = (datenum: number) => { interface BlueprintLinkProps { blueprint: BlueprintPage; editLink?: boolean; + type: "tile" | "row"; } -export const BlueprintLink: React.FC = ({ blueprint, editLink }) => ( -
- - - - {blueprint.title} - - - - {blueprint.favorite_count} - - {formatDate(blueprint.updated_at)} +export const BlueprintLink: React.FC = ({ + blueprint, + editLink, + type = "tile", +}) => { + const [imageError, setImageError] = useState(false); + const onImageError = (error: unknown) => { + console.log(error); + setImageError(true); + }; + + return ( + -); + + +
+ ); +}; diff --git a/apps/blueprints/src/components/BookChildTree.tsx b/apps/blueprints/src/components/BookChildTree.tsx index 80817d0..789cc26 100644 --- a/apps/blueprints/src/components/BookChildTree.tsx +++ b/apps/blueprints/src/components/BookChildTree.tsx @@ -1,7 +1,7 @@ import { css } from "@emotion/react"; import Link from "next/link"; import BBCode from "bbcode-to-react"; -import { ChildTree } from "@factorio-sites/database"; +import { ChildTree } from "@factorio-sites/types"; const componentStyles = css` .blueprint, diff --git a/apps/blueprints/src/components/ImageEditor.tsx b/apps/blueprints/src/components/ImageEditor.tsx index 3d149fe..6fa611c 100644 --- a/apps/blueprints/src/components/ImageEditor.tsx +++ b/apps/blueprints/src/components/ImageEditor.tsx @@ -70,7 +70,7 @@ export const ImageEditor: React.FC<{ string: string }> = ({ string }) => { return (
- + {/* blueprint */}
); diff --git a/apps/blueprints/src/pages/api/blueprint/create.ts b/apps/blueprints/src/pages/api/blueprint/create.ts index f5a7c6b..8d9138b 100644 --- a/apps/blueprints/src/pages/api/blueprint/create.ts +++ b/apps/blueprints/src/pages/api/blueprint/create.ts @@ -2,8 +2,9 @@ import { createBlueprint, createBlueprintPage, createBlueprintBook, + getBlueprintById, } from "@factorio-sites/database"; -import { parseBlueprintString } from "@factorio-sites/node-utils"; +import { getFirstBlueprintFromChildTree, parseBlueprintString } from "@factorio-sites/node-utils"; import { parseDatabaseError } from "../../../utils/api.utils"; import { apiHandler } from "../../../utils/api-handler"; @@ -37,12 +38,22 @@ const handler = apiHandler(async (req, res, { session }) => { }; if (parsed?.data.blueprint) { - const { insertedId } = await createBlueprint(parsed.data.blueprint, info); - const page = await createBlueprintPage("blueprint", insertedId, info); + const blueprint = await createBlueprint(parsed.data.blueprint, info); + const page = await createBlueprintPage("blueprint", blueprint.id, { + ...info, + image_hash: blueprint.image_hash, + }); return res.status(201).json({ success: true, id: page.id }); } else if (parsed?.data.blueprint_book) { - const { insertedId } = await createBlueprintBook(parsed.data.blueprint_book, info); - const page = await createBlueprintPage("blueprint_book", insertedId, info); + const result = await createBlueprintBook(parsed.data.blueprint_book, info); + const firstBlueprintId = getFirstBlueprintFromChildTree(result.child_tree); + const firstBlueprint = await getBlueprintById(firstBlueprintId); + if (!firstBlueprint) throw Error("Failed to find blueprint"); + const page = await createBlueprintPage("blueprint_book", result.id, { + ...info, + image_hash: firstBlueprint.image_hash, + firstBlueprintId, + }); return res.status(201).json({ success: true, id: page.id }); } } catch (reason) { diff --git a/apps/blueprints/src/pages/api/blueprint/edit.ts b/apps/blueprints/src/pages/api/blueprint/edit.ts index bab063e..3569826 100644 --- a/apps/blueprints/src/pages/api/blueprint/edit.ts +++ b/apps/blueprints/src/pages/api/blueprint/edit.ts @@ -34,12 +34,12 @@ const handler = apiHandler(async (req, res, { session }) => { console.log(info); if (parsed?.data.blueprint) { - const { insertedId } = await createBlueprint(parsed.data.blueprint, info); - const page = await editBlueprintPage(id, "blueprint", insertedId, info); + const result = await createBlueprint(parsed.data.blueprint, info); + const page = await editBlueprintPage(id, "blueprint", result.id, info); return res.status(200).json({ success: true, id: page.id }); } else if (parsed?.data.blueprint_book) { - const { insertedId } = await createBlueprintBook(parsed.data.blueprint_book, info); - const page = await editBlueprintPage(id, "blueprint_book", insertedId, info); + const result = await createBlueprintBook(parsed.data.blueprint_book, info); + const page = await editBlueprintPage(id, "blueprint_book", result.id, info); return res.status(200).json({ success: true, id: page.id }); } } catch (reason) { diff --git a/apps/blueprints/src/pages/api/blueprint/thumbnail.ts b/apps/blueprints/src/pages/api/blueprint/thumbnail.ts new file mode 100644 index 0000000..a078370 --- /dev/null +++ b/apps/blueprints/src/pages/api/blueprint/thumbnail.ts @@ -0,0 +1,36 @@ +import { NextApiHandler } from "next"; +import { + getBlueprintBookById, + getBlueprintById, + getBlueprintImageRequestTopic, + getBlueprintPageById, +} from "@factorio-sites/database"; +import { getFirstBlueprintFromChildTree } from "@factorio-sites/node-utils"; +import { apiHandler, ApiError } from "../../../utils/api-handler"; + +const handler: NextApiHandler = apiHandler(async (req, res) => { + if (!req.query.id) throw new ApiError(400, "No id in query"); + const blueprintPage = await getBlueprintPageById(req.query.id as string); + if (!blueprintPage) return res.status(404).json({ error: "Blueprint page not found" }); + + const blueprintImageRequestTopic = getBlueprintImageRequestTopic(); + + if (blueprintPage.blueprint_id) { + blueprintImageRequestTopic.publishJSON({ + blueprintId: blueprintPage.blueprint_id, + }); + return res.json({ blueprint_id: blueprintPage.blueprint_id }); + } else if (blueprintPage.blueprint_book_id) { + const blueprintBook = await getBlueprintBookById(blueprintPage.blueprint_book_id); + if (!blueprintBook) throw new ApiError(500, "Failed to find blueprint book"); + const firstBlueprintId = getFirstBlueprintFromChildTree(blueprintBook.child_tree); + const firstBlueprint = await getBlueprintById(firstBlueprintId); + if (!firstBlueprint) throw new ApiError(500, "Failed to find blueprint"); + blueprintImageRequestTopic.publishJSON({ + blueprintId: firstBlueprintId, + }); + return res.json({ blueprint_id: firstBlueprintId }); + } +}); + +export default handler; diff --git a/apps/blueprints/src/pages/api/image-to-gen.ts b/apps/blueprints/src/pages/api/image-to-gen.ts index 0434f97..f44ea17 100644 --- a/apps/blueprints/src/pages/api/image-to-gen.ts +++ b/apps/blueprints/src/pages/api/image-to-gen.ts @@ -5,8 +5,10 @@ import { getBlueprintById, getBlueprintStringByHash, hasBlueprintImage, - Blueprint, } from "@factorio-sites/database"; +import { Blueprint } from "@factorio-sites/types"; + +const DISABLED = true; const getOneMessage = async (): Promise => { const topic = getBlueprintImageRequestTopic(); @@ -43,6 +45,8 @@ const getOneMessage = async (): Promise => { }; const handler: NextApiHandler = async (req, res) => { + if (DISABLED) return res.status(400).send("Method not availablee"); + // Allow the url to be used in the blueprint editor if ( req.headers.origin && diff --git a/apps/blueprints/src/pages/api/openid/return.ts b/apps/blueprints/src/pages/api/openid/return.ts index 5ff18f5..9cdd02e 100644 --- a/apps/blueprints/src/pages/api/openid/return.ts +++ b/apps/blueprints/src/pages/api/openid/return.ts @@ -33,15 +33,6 @@ const handler: NextApiHandler = async (req, res) => { if (user) { const session = await createSession(user, useragent, ip); setUserToken(res, session.session_token); - - // Redirect from in browser to let the cookie be stored - // If not, the first render still happens as guest - res.setHeader("content-type", "text/html"); - return res.status(200).end(` - - - - `); } // First time logging in, make new user else { @@ -54,9 +45,16 @@ const handler: NextApiHandler = async (req, res) => { const session = await createSession(user, useragent, ip); setUserToken(res, session.session_token); - res.setHeader("Location", `${process.env.BASE_URL}`); - return res.status(302).end(); } + + // Redirect from in browser to let the cookie be stored + // If not, the first render still happens as guest + res.setHeader("content-type", "text/html"); + return res.status(200).end(` + + + + `); }; export default handler; diff --git a/apps/blueprints/src/pages/blueprint/[blueprintId].tsx b/apps/blueprints/src/pages/blueprint/[blueprintId].tsx index 040fb06..2c67164 100644 --- a/apps/blueprints/src/pages/blueprint/[blueprintId].tsx +++ b/apps/blueprints/src/pages/blueprint/[blueprintId].tsx @@ -3,14 +3,12 @@ import { NextPage } from "next"; import BBCode from "bbcode-to-react"; import { Button, Grid, Image } from "@chakra-ui/react"; import { - BlueprintBook, - Blueprint, - BlueprintPage, getBlueprintBookById, getBlueprintById, getBlueprintPageById, isBlueprintPageUserFavorite, } from "@factorio-sites/database"; +import { BlueprintBook, Blueprint, BlueprintPage } from "@factorio-sites/types"; import { BlueprintStringData, timeLogger } from "@factorio-sites/common-utils"; import { chakraResponsive, parseBlueprintStringClient } from "@factorio-sites/web-utils"; import { Panel } from "../../components/Panel"; @@ -24,11 +22,10 @@ import styled from "@emotion/styled"; import { AiOutlineHeart, AiFillHeart } from "react-icons/ai"; type Selected = - | { type: "blueprint"; data: Pick } - | { type: "blueprint_book"; data: Pick }; + | { type: "blueprint"; data: Pick } + | { type: "blueprint_book"; data: Pick }; interface IndexProps { - image_exists: boolean; selected: Selected; blueprint: Blueprint | null; blueprint_book: BlueprintBook | null; @@ -46,7 +43,6 @@ const StyledTable = styled.table` `; export const Index: NextPage = ({ - image_exists, selected, blueprint, blueprint_book, @@ -90,7 +86,6 @@ export const Index: NextPage = ({ useEffect(() => { console.log({ - image_exists, selected, blueprint, blueprint_book, @@ -149,24 +144,20 @@ export const Index: NextPage = ({ gridColumn="1" > {blueprint_book ? ( - <> -
This string contains a blueprint book
-
-
- -
- +
+ +
) : blueprint ? ( {blueprint_page.description_markdown} ) : null} @@ -216,12 +207,10 @@ export const Index: NextPage = ({ {selected.type === "blueprint" && data?.blueprint && ( - Entities for{" "} - {data.blueprint.label ? BBCode.toReact(data.blueprint.label) : "blueprint"} - - ) as any + + Entities for{" "} + {data.blueprint.label ? BBCode.toReact(data.blueprint.label) : "blueprint"} + } gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })} > @@ -257,7 +246,10 @@ export const Index: NextPage = ({ )} - + <> {blueprintString && }