1
0
mirror of https://github.com/barthuijgen/factorio-sites.git synced 2024-11-21 18:16:33 +02:00

blueprint managent updates and a lot more

This commit is contained in:
Bart Huijgen 2021-03-06 22:54:18 +01:00
parent 00358228dc
commit 512b66300d
59 changed files with 935 additions and 819 deletions

View File

@ -1,6 +1,3 @@
{ {
"recommendations": [ "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
} }

View File

@ -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"
}
}

View File

@ -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<string, string>;
// 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<string, any>;
/** 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<string, unknown> = {}) => {
console.error(JSON.stringify({ message, ...data }, jsonReplaceErrors));
callback(message);
};
const log = (message: string, data: Record<string, unknown> = {}) => {
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 });
}
};

View File

@ -1,50 +1,27 @@
import * as sharp from "sharp"; import * as sharp from "sharp";
const RESIZE_ENABLED = false;
// const calculateImageSizeMod = (pixels: number) => // const calculateImageSizeMod = (pixels: number) =>
// Math.min(Math.max((-pixels + 500) / 20500 + 1, 0.3), 1); // 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) => export const optimise = async (image: Buffer, max_dimention = 5000): Promise<Buffer> => {
Math.min(Math.max((-pixels + 3000) / 33000 + 1, 0.3), 1); 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<Buffer> => { console.log(
let sharp_image = sharp(image); JSON.stringify({
if (RESIZE_ENABLED) { input_size: `${Math.round(image.byteLength / 1024) / 1000}mb`,
const MAX_IMAGE_DIMENTION = 5000; output_size: `${Math.round(sharp_image.byteLength / 1024) / 1000}mb`,
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 });
}
const min_image = await sharp_image.toBuffer(); return sharp_image;
console.log({
input_size_mb: image.byteLength / 1024_000,
output_size_mb: min_image.byteLength / 1024_000,
});
return min_image;
}; };

View File

@ -4,11 +4,11 @@ import * as Puppeteer from "puppeteer";
let BROWSER: Puppeteer.Browser; let BROWSER: Puppeteer.Browser;
let PAGE: Puppeteer.Page; let PAGE: Puppeteer.Page;
async function getPage() { async function getPage(headless: boolean) {
if (PAGE) return PAGE; if (PAGE) return PAGE;
const _browser = await Puppeteer.launch({ const _browser = await Puppeteer.launch({
headless: false, headless,
args: ["--no-sandbox"], args: ["--no-sandbox"],
}); });
@ -24,9 +24,9 @@ async function getPage() {
return _page; return _page;
} }
export async function renderImage(blueprint_string: string) { export async function renderImage(blueprint_string: string, options?: { headless: boolean }) {
const tl = timeLogger("localFbeRenderer"); const tl = timeLogger("localFbeRenderer");
const page = await getPage(); const page = await getPage(options?.headless ?? true);
tl("Page loaded"); tl("Page loaded");

View File

@ -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();
}

View File

@ -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");
}

View File

@ -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)); // import { subscribeToPubSub } from "./pubsub-render";
// uploadLocalFiles().catch((reason) => console.error("Fatal error:", reason)); // subscribeToPubSub().catch((reason) => console.error("Fatal error:", reason));
// rePublishAllBlueprints().catch((reason) => console.error("Fatal error:", reason)); // rePublishAllBlueprints().catch((reason) => console.error("Fatal error:", reason));
// image hash = a99525f97c26c7242ecdd96679043b1a5e65dd0c exports.renderImageHttp = functionHttpHandler;
// SELECT * FROM BlueprintBook WHERE blueprint_ids CONTAINS Key(Blueprint, 4532736400293888) exports.renderImagePubSub = functionPubSubHandler;
// bp = Key(Blueprint, 4532736400293888)
// book = Key(BlueprintBook, 5034207050989568) // local_test("8737437e-f15b-459c-8c1d-d0074f3a89ca");
// page = 6225886932107264

View File

@ -35,11 +35,12 @@ export async function subscribeToPubSub() {
}; };
try { try {
const data = JSON.parse(message.data.toString()); if (!message.attributes.blueprintId) {
if (!data.blueprintId) return ack("blueprintId not found in message body", false); return ack("blueprintId not found in message body", false);
}
console.log("------------------------------------------------"); console.log("------------------------------------------------");
console.log("[pubsub] generating image for", data.blueprintId); console.log("[pubsub] generating image for", message.attributes.blueprintId);
const blueprint = await getBlueprintById(data.blueprintId); const blueprint = await getBlueprintById(message.attributes.blueprintId);
if (!blueprint) return ack("Blueprint not found", false); if (!blueprint) return ack("Blueprint not found", false);
if (await hasBlueprintImage(blueprint.image_hash)) { if (await hasBlueprintImage(blueprint.image_hash)) {
@ -58,7 +59,7 @@ export async function subscribeToPubSub() {
return ack("[pubsub] image saved", true); return ack("[pubsub] image saved", true);
} catch (reason) { } catch (reason) {
return ack(`[pubsub:error] ${reason}`, false); return ack(`[pubsub:error] ${reason.stack || reason}`, false);
} }
}; };

View File

@ -1,20 +1,22 @@
import { getBlueprintImageRequestTopic, getPaginatedBlueprints } from "@factorio-sites/database"; // import { getBlueprintImageRequestTopic, getPaginatedBlueprints } from "@factorio-sites/database";
export async function rePublishAllBlueprints() { // export async function rePublishAllBlueprints() {
const topic = getBlueprintImageRequestTopic(); // const topic = getBlueprintImageRequestTopic();
const fetchPage = async (page = 1) => { // const fetchPage = async (page = 1) => {
const blueprints = await getPaginatedBlueprints(page); // const blueprints = await getPaginatedBlueprints(page);
if (blueprints.length === 0) { // if (blueprints.length === 0) {
return console.log("No more blueprints found"); // return console.log("No more blueprints found");
} // }
console.log(`Publishing page ${page} with ${blueprints.length} blueprints`); // console.log(`Publishing page ${page} with ${blueprints.length} blueprints`);
await Promise.all( // await Promise.all(
blueprints.map((blueprint) => { // blueprints.map((blueprint) => {
return topic.publishJSON({ blueprintId: blueprint.id }); // return topic.publishJSON({ blueprintId: blueprint.id });
}) // })
); // );
fetchPage(page + 1); // fetchPage(page + 1);
}; // };
await fetchPage(); // await fetchPage();
} // }
export {};

View File

@ -2,6 +2,7 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../../dist/out-tsc", "outDir": "../../dist/out-tsc",
"target": "ES2019",
"types": ["node"] "types": ["node"]
}, },
"exclude": ["**/*.spec.ts"], "exclude": ["**/*.spec.ts"],

View File

@ -1,14 +1,17 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
module.exports = { module.exports = {
poweredByHeader: false, poweredByHeader: false,
reactStrictMode: true, reactStrictMode: true,
images: {
domains: ["storage.googleapis.com"],
},
webpack(config, options) { webpack(config, options) {
const { dev, isServer } = options; const { dev, isServer } = options;
// Do not run type checking twice: // Do not run type checking twice:
if (dev && isServer) { if (dev && isServer) {
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
config.plugins.push(new ForkTsCheckerWebpackPlugin()); config.plugins.push(new ForkTsCheckerWebpackPlugin());
} }

View File

@ -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");

View File

@ -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;

View File

@ -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;

View File

@ -14,43 +14,33 @@ enum enum_user_role {
} }
model blueprint { model blueprint {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
label String? @db.VarChar(255) label String? @db.VarChar(255)
description String? description String?
game_version String? @db.VarChar(255) game_version String? @db.VarChar(255)
blueprint_hash String @unique @db.VarChar(40) blueprint_hash String @unique @db.VarChar(40)
image_hash String @db.VarChar(40) image_hash String @db.VarChar(40)
image_version Int @default(1) image_version Int @default(1)
tags String[] @db.VarChar(255) tags String[] @db.VarChar(255)
created_at DateTime @default(now()) created_at DateTime @default(now())
updated_at DateTime @updatedAt updated_at DateTime @updatedAt
blueprint_page blueprint_page? blueprint_page blueprint_page?
blueprint_books blueprint_book[]
} }
model blueprint_book { model blueprint_book {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
label String? @db.VarChar(255) label String? @db.VarChar(255)
description String? description String?
child_tree Json @db.Json child_tree Json @db.Json
blueprint_hash String @unique @db.VarChar(40) blueprint_hash String @unique @db.VarChar(40)
is_modded Boolean is_modded Boolean
created_at DateTime @default(now()) created_at DateTime @default(now())
updated_at DateTime @updatedAt updated_at DateTime @updatedAt
blueprint_page blueprint_page? blueprint_page blueprint_page?
} blueprints blueprint[]
blueprint_books blueprint_book[] @relation("blueprint_books")
model blueprint_book_blueprints { blueprint_books_relation blueprint_book[] @relation("blueprint_books")
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])
} }
model blueprint_page { model blueprint_page {
@ -61,6 +51,7 @@ model blueprint_page {
title String @db.VarChar(255) title String @db.VarChar(255)
description_markdown String? description_markdown String?
tags String[] @db.VarChar(255) tags String[] @db.VarChar(255)
image_hash String @db.VarChar(40)
factorioprints_id String? @unique @db.VarChar(255) factorioprints_id String? @unique @db.VarChar(255)
created_at DateTime @default(now()) created_at DateTime @default(now())
updated_at DateTime @updatedAt updated_at DateTime @updatedAt

View File

@ -1,21 +1,22 @@
{ {
"dependencies": { "dependencies": {
"@chakra-ui/react": "1.1.2", "@chakra-ui/react": "1.3.3",
"@emotion/react": "11.1.4", "@emotion/react": "11.1.5",
"@emotion/server": "11.0.0", "@emotion/server": "11.0.0",
"@emotion/styled": "11.0.0", "@emotion/styled": "11.1.5",
"@fbe/editor": "./fbe-editor-v1.0.0.tgz", "@fbe/editor": "file:.yalc/@fbe/editor",
"@google-cloud/pubsub": "2.7.0", "@google-cloud/pubsub": "2.9.0",
"@google-cloud/secret-manager": "3.2.3", "@google-cloud/secret-manager": "3.4.0",
"@google-cloud/storage": "5.7.0", "@google-cloud/storage": "5.7.4",
"@hookstate/core": "3.0.3", "@hookstate/core": "3.0.5",
"@prisma/client": "2.18.0",
"bbcode-to-react": "0.2.9", "bbcode-to-react": "0.2.9",
"bcrypt": "5.0.0", "bcrypt": "5.0.0",
"cookie": "0.4.1", "cookie": "0.4.1",
"document-register-element": "1.14.10", "document-register-element": "1.14.10",
"formik": "2.2.6", "formik": "2.2.6",
"framer-motion": "3.1.4", "framer-motion": "3.3.0",
"next": "10.0.5", "next": "10.0.7",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"openid": "2.0.7", "openid": "2.0.7",
"pako": "1.0.11", "pako": "1.0.11",
@ -23,9 +24,10 @@
"phin": "3.5.1", "phin": "3.5.1",
"react": "17.0.1", "react": "17.0.1",
"react-dom": "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-map-interaction": "2.0.0",
"react-markdown": "5.0.3", "react-markdown": "5.0.3",
"sequelize": "6.3.5" "react-multi-select-component": "3.1.3",
"fork-ts-checker-webpack-plugin": "6.1.1"
} }
} }

View File

@ -1,20 +1,45 @@
import Link from "next/link"; import Link from "next/link";
import Image from "next/image";
import { css } from "@emotion/react"; 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 { Box, Text } from "@chakra-ui/react";
import { MdFavorite } from "react-icons/md"; import { MdFavorite } from "react-icons/md";
import { useState } from "react";
const linkStyles = css` const linkStyles = css`
width: 100%; margin: 5px 10px 5px 0;
margin: 5px 0; background: #353535;
.block {
display: flex;
justify-content: space-between;
}
.image {
position: relative;
width: 200px;
height: 200px;
}
.details {
display: flex;
}
a { a {
display: block; display: block;
padding: 5px; padding: 5px;
color: #fff; color: #fff;
} }
&:hover { &:hover {
cursor: pointer; cursor: pointer;
background: #ccc; background: #4c4c4c;
}
&.tile {
.block {
flex-direction: column;
}
} }
`; `;
@ -26,26 +51,61 @@ const formatDate = (datenum: number) => {
interface BlueprintLinkProps { interface BlueprintLinkProps {
blueprint: BlueprintPage; blueprint: BlueprintPage;
editLink?: boolean; editLink?: boolean;
type: "tile" | "row";
} }
export const BlueprintLink: React.FC<BlueprintLinkProps> = ({ blueprint, editLink }) => ( export const BlueprintLink: React.FC<BlueprintLinkProps> = ({
<div css={linkStyles}> blueprint,
<Link editLink,
href={editLink ? `/user/blueprint/${blueprint.id}` : `/blueprint/${blueprint.id}`} type = "tile",
passHref }) => {
> const [imageError, setImageError] = useState(false);
<a> const onImageError = (error: unknown) => {
<Box css={{ display: "flex", justifyContent: "space-between" }}> console.log(error);
<Text>{blueprint.title}</Text> setImageError(true);
<Box css={{ display: "flex" }}> };
<Text css={{ display: "flex", alignItems: "center", marginRight: "2rem" }}>
<MdFavorite css={{ marginRight: "0.5rem" }} /> return (
{blueprint.favorite_count} <div css={linkStyles} className={type}>
</Text> <Link
<Text>{formatDate(blueprint.updated_at)}</Text> href={editLink ? `/user/blueprint/${blueprint.id}` : `/blueprint/${blueprint.id}`}
passHref
>
<a>
<Box className="block">
{type === "tile" && (
<div className="image">
{imageError ? (
<div>
Looks like this image can\t load. <button>Try generating it again</button>
</div>
) : (
<Image
loader={({ src }) => src}
src={`https://storage.googleapis.com/blueprint-images/300/${blueprint.image_hash}.webp`}
layout="fill"
objectFit="contain"
alt={blueprint.title}
onError={onImageError}
/>
)}
</div>
)}
<Box className="details">
<Text css={{ display: "flex", alignItems: "center", marginRight: "1rem" }}>
<MdFavorite css={{ marginRight: "5px" }} />
{blueprint.favorite_count || "0"}
</Text>
<Text>{blueprint.title}</Text>
</Box>
{type === "row" && (
<Box>
<Text>{formatDate(blueprint.updated_at)}</Text>
</Box>
)}
</Box> </Box>
</Box> </a>
</a> </Link>
</Link> </div>
</div> );
); };

View File

@ -1,7 +1,7 @@
import { css } from "@emotion/react"; import { css } from "@emotion/react";
import Link from "next/link"; import Link from "next/link";
import BBCode from "bbcode-to-react"; import BBCode from "bbcode-to-react";
import { ChildTree } from "@factorio-sites/database"; import { ChildTree } from "@factorio-sites/types";
const componentStyles = css` const componentStyles = css`
.blueprint, .blueprint,

View File

@ -70,7 +70,7 @@ export const ImageEditor: React.FC<{ string: string }> = ({ string }) => {
return ( return (
<div> <div>
<canvas ref={canvasRef} style={{ width: "100%", height: "auto" }} /> <canvas id="pbe" ref={canvasRef} style={{ width: "100%", height: "auto" }} />
{/* <img src={image} alt="blueprint" style={{ width: "500px" }}></img> */} {/* <img src={image} alt="blueprint" style={{ width: "500px" }}></img> */}
</div> </div>
); );

View File

@ -2,8 +2,9 @@ import {
createBlueprint, createBlueprint,
createBlueprintPage, createBlueprintPage,
createBlueprintBook, createBlueprintBook,
getBlueprintById,
} from "@factorio-sites/database"; } 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 { parseDatabaseError } from "../../../utils/api.utils";
import { apiHandler } from "../../../utils/api-handler"; import { apiHandler } from "../../../utils/api-handler";
@ -37,12 +38,22 @@ const handler = apiHandler(async (req, res, { session }) => {
}; };
if (parsed?.data.blueprint) { if (parsed?.data.blueprint) {
const { insertedId } = await createBlueprint(parsed.data.blueprint, info); const blueprint = await createBlueprint(parsed.data.blueprint, info);
const page = await createBlueprintPage("blueprint", insertedId, info); const page = await createBlueprintPage("blueprint", blueprint.id, {
...info,
image_hash: blueprint.image_hash,
});
return res.status(201).json({ success: true, id: page.id }); return res.status(201).json({ success: true, id: page.id });
} else if (parsed?.data.blueprint_book) { } else if (parsed?.data.blueprint_book) {
const { insertedId } = await createBlueprintBook(parsed.data.blueprint_book, info); const result = await createBlueprintBook(parsed.data.blueprint_book, info);
const page = await createBlueprintPage("blueprint_book", insertedId, 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 }); return res.status(201).json({ success: true, id: page.id });
} }
} catch (reason) { } catch (reason) {

View File

@ -34,12 +34,12 @@ const handler = apiHandler(async (req, res, { session }) => {
console.log(info); console.log(info);
if (parsed?.data.blueprint) { if (parsed?.data.blueprint) {
const { insertedId } = await createBlueprint(parsed.data.blueprint, info); const result = await createBlueprint(parsed.data.blueprint, info);
const page = await editBlueprintPage(id, "blueprint", insertedId, info); const page = await editBlueprintPage(id, "blueprint", result.id, info);
return res.status(200).json({ success: true, id: page.id }); return res.status(200).json({ success: true, id: page.id });
} else if (parsed?.data.blueprint_book) { } else if (parsed?.data.blueprint_book) {
const { insertedId } = await createBlueprintBook(parsed.data.blueprint_book, info); const result = await createBlueprintBook(parsed.data.blueprint_book, info);
const page = await editBlueprintPage(id, "blueprint_book", insertedId, info); const page = await editBlueprintPage(id, "blueprint_book", result.id, info);
return res.status(200).json({ success: true, id: page.id }); return res.status(200).json({ success: true, id: page.id });
} }
} catch (reason) { } catch (reason) {

View File

@ -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;

View File

@ -5,8 +5,10 @@ import {
getBlueprintById, getBlueprintById,
getBlueprintStringByHash, getBlueprintStringByHash,
hasBlueprintImage, hasBlueprintImage,
Blueprint,
} from "@factorio-sites/database"; } from "@factorio-sites/database";
import { Blueprint } from "@factorio-sites/types";
const DISABLED = true;
const getOneMessage = async (): Promise<Blueprint> => { const getOneMessage = async (): Promise<Blueprint> => {
const topic = getBlueprintImageRequestTopic(); const topic = getBlueprintImageRequestTopic();
@ -43,6 +45,8 @@ const getOneMessage = async (): Promise<Blueprint> => {
}; };
const handler: NextApiHandler = async (req, res) => { 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 // Allow the url to be used in the blueprint editor
if ( if (
req.headers.origin && req.headers.origin &&

View File

@ -33,15 +33,6 @@ const handler: NextApiHandler = async (req, res) => {
if (user) { if (user) {
const session = await createSession(user, useragent, ip); const session = await createSession(user, useragent, ip);
setUserToken(res, session.session_token); 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(`
<html><head>
<meta http-equiv="refresh" content="0;url=${process.env.BASE_URL}" />
</head></html>
`);
} }
// First time logging in, make new user // First time logging in, make new user
else { else {
@ -54,9 +45,16 @@ const handler: NextApiHandler = async (req, res) => {
const session = await createSession(user, useragent, ip); const session = await createSession(user, useragent, ip);
setUserToken(res, session.session_token); 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(`
<html><head>
<meta http-equiv="refresh" content="0;url=${process.env.BASE_URL}" />
</head></html>
`);
}; };
export default handler; export default handler;

View File

@ -3,14 +3,12 @@ import { NextPage } from "next";
import BBCode from "bbcode-to-react"; import BBCode from "bbcode-to-react";
import { Button, Grid, Image } from "@chakra-ui/react"; import { Button, Grid, Image } from "@chakra-ui/react";
import { import {
BlueprintBook,
Blueprint,
BlueprintPage,
getBlueprintBookById, getBlueprintBookById,
getBlueprintById, getBlueprintById,
getBlueprintPageById, getBlueprintPageById,
isBlueprintPageUserFavorite, isBlueprintPageUserFavorite,
} from "@factorio-sites/database"; } from "@factorio-sites/database";
import { BlueprintBook, Blueprint, BlueprintPage } from "@factorio-sites/types";
import { BlueprintStringData, timeLogger } from "@factorio-sites/common-utils"; import { BlueprintStringData, timeLogger } from "@factorio-sites/common-utils";
import { chakraResponsive, parseBlueprintStringClient } from "@factorio-sites/web-utils"; import { chakraResponsive, parseBlueprintStringClient } from "@factorio-sites/web-utils";
import { Panel } from "../../components/Panel"; import { Panel } from "../../components/Panel";
@ -24,11 +22,10 @@ import styled from "@emotion/styled";
import { AiOutlineHeart, AiFillHeart } from "react-icons/ai"; import { AiOutlineHeart, AiFillHeart } from "react-icons/ai";
type Selected = type Selected =
| { type: "blueprint"; data: Pick<Blueprint, "id" | "blueprint_hash" | "image_hash"> } | { type: "blueprint"; data: Pick<Blueprint, "id" | "blueprint_hash" | "image_hash" | "label"> }
| { type: "blueprint_book"; data: Pick<BlueprintBook, "id" | "blueprint_hash"> }; | { type: "blueprint_book"; data: Pick<BlueprintBook, "id" | "blueprint_hash" | "label"> };
interface IndexProps { interface IndexProps {
image_exists: boolean;
selected: Selected; selected: Selected;
blueprint: Blueprint | null; blueprint: Blueprint | null;
blueprint_book: BlueprintBook | null; blueprint_book: BlueprintBook | null;
@ -46,7 +43,6 @@ const StyledTable = styled.table`
`; `;
export const Index: NextPage<IndexProps> = ({ export const Index: NextPage<IndexProps> = ({
image_exists,
selected, selected,
blueprint, blueprint,
blueprint_book, blueprint_book,
@ -90,7 +86,6 @@ export const Index: NextPage<IndexProps> = ({
useEffect(() => { useEffect(() => {
console.log({ console.log({
image_exists,
selected, selected,
blueprint, blueprint,
blueprint_book, blueprint_book,
@ -149,24 +144,20 @@ export const Index: NextPage<IndexProps> = ({
gridColumn="1" gridColumn="1"
> >
{blueprint_book ? ( {blueprint_book ? (
<> <div css={{ maxHeight: "400px", overflow: "auto" }}>
<div>This string contains a blueprint book </div> <BookChildTree
<br /> child_tree={[
<div css={{ maxHeight: "400px", overflow: "auto" }}> {
<BookChildTree id: blueprint_book.id,
child_tree={[ name: blueprint_book.label,
{ type: "blueprint_book",
id: blueprint_book.id, children: blueprint_book.child_tree,
name: blueprint_book.label, },
type: "blueprint_book", ]}
children: blueprint_book.child_tree, base_url={`/blueprint/${blueprint_page.id}`}
}, selected_id={selected.data.id}
]} />
base_url={`/blueprint/${blueprint_page.id}`} </div>
selected_id={selected.data.id}
/>
</div>
</>
) : blueprint ? ( ) : blueprint ? (
<Markdown>{blueprint_page.description_markdown}</Markdown> <Markdown>{blueprint_page.description_markdown}</Markdown>
) : null} ) : null}
@ -216,12 +207,10 @@ export const Index: NextPage<IndexProps> = ({
{selected.type === "blueprint" && data?.blueprint && ( {selected.type === "blueprint" && data?.blueprint && (
<Panel <Panel
title={ title={
( <span>
<span> Entities for{" "}
Entities for{" "} {data.blueprint.label ? BBCode.toReact(data.blueprint.label) : "blueprint"}
{data.blueprint.label ? BBCode.toReact(data.blueprint.label) : "blueprint"} </span>
</span>
) as any
} }
gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })} gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })}
> >
@ -257,7 +246,10 @@ export const Index: NextPage<IndexProps> = ({
</StyledTable> </StyledTable>
</Panel> </Panel>
)} )}
<Panel title="string" gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })}> <Panel
title={`string for ${selected.type.replace("_", " ")} "${selected.data.label}"`}
gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })}
>
<> <>
{blueprintString && <CopyButton content={blueprintString} marginBottom="0.5rem" />} {blueprintString && <CopyButton content={blueprintString} marginBottom="0.5rem" />}
<textarea <textarea
@ -370,6 +362,7 @@ export const getServerSideProps = pageHandler(async (context, { session }) => {
id: selected_blueprint.id, id: selected_blueprint.id,
blueprint_hash: selected_blueprint.blueprint_hash, blueprint_hash: selected_blueprint.blueprint_hash,
image_hash: selected_blueprint.image_hash, image_hash: selected_blueprint.image_hash,
label: selected_blueprint.label,
}, },
}; };
} else if (selected_blueprint_book) { } else if (selected_blueprint_book) {
@ -378,20 +371,17 @@ export const getServerSideProps = pageHandler(async (context, { session }) => {
data: { data: {
id: selected_blueprint_book.id, id: selected_blueprint_book.id,
blueprint_hash: selected_blueprint_book.blueprint_hash, blueprint_hash: selected_blueprint_book.blueprint_hash,
label: selected_blueprint_book.label,
}, },
}; };
} }
// const image_exists =
// selected.type === "blueprint" ? await hasBlueprintImage(selected.data.image_hash) : false;
const favorite = session const favorite = session
? !!(await isBlueprintPageUserFavorite(session.user.id, blueprint_page.id)) ? !!(await isBlueprintPageUserFavorite(session.user.id, blueprint_page.id))
: false; : false;
return { return {
props: { props: {
image_exists: false,
blueprint, blueprint,
blueprint_book, blueprint_book,
selected, selected,

View File

@ -1,12 +1,13 @@
import React from "react"; import React from "react";
import { NextPage, NextPageContext } from "next"; import { NextPage, NextPageContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { BlueprintPage, searchBlueprintPages, init } from "@factorio-sites/database"; import { useRouter } from "next/router";
import { searchBlueprintPages, init } from "@factorio-sites/database";
import { SimpleGrid, Box, RadioGroup, Stack, Radio } from "@chakra-ui/react"; import { SimpleGrid, Box, RadioGroup, Stack, Radio } from "@chakra-ui/react";
import { BlueprintPage } from "@factorio-sites/types";
import { Panel } from "../components/Panel"; import { Panel } from "../components/Panel";
import { Pagination } from "../components/Pagination"; import { Pagination } from "../components/Pagination";
import { useRouterQueryToHref } from "../hooks/query.hook"; import { useRouterQueryToHref } from "../hooks/query.hook";
import { useRouter } from "next/router";
import { BlueprintLink } from "../components/BlueprintLink"; import { BlueprintLink } from "../components/BlueprintLink";
import { TagsSelect } from "../components/TagsSelect"; import { TagsSelect } from "../components/TagsSelect";
import { queryValueAsArray } from "../utils/query.utils"; import { queryValueAsArray } from "../utils/query.utils";
@ -67,10 +68,12 @@ export const Index: NextPage<IndexProps> = ({
</Box> </Box>
)} )}
</Box> </Box>
<Box> <Box css={{ display: "flex" }}>
{blueprints.map((bp) => ( {blueprints.map((bp) => (
<BlueprintLink key={bp.id} blueprint={bp} /> <BlueprintLink key={bp.id} blueprint={bp} type="tile" />
))} ))}
</Box>
<Box>
<Pagination page={currentPage} totalPages={totalPages} totalItems={totalItems} /> <Pagination page={currentPage} totalPages={totalPages} totalItems={totalItems} />
</Box> </Box>
</Panel> </Panel>

View File

@ -14,11 +14,12 @@ import {
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { Formik, Field } from "formik"; import { Formik, Field } from "formik";
import { css } from "@emotion/react"; import { css } from "@emotion/react";
import { chakraResponsive } from "@factorio-sites/web-utils";
import { Panel } from "../../components/Panel"; import { Panel } from "../../components/Panel";
import { validateCreateBlueprintForm } from "../../utils/validate"; import { validateCreateBlueprintForm } from "../../utils/validate";
import { useAuth } from "../../providers/auth"; import { useAuth } from "../../providers/auth";
import { ImageEditor } from "../../components/ImageEditor"; import { ImageEditor } from "../../components/ImageEditor";
import { chakraResponsive } from "@factorio-sites/web-utils"; import { TagsSelect } from "../../components/TagsSelect";
const FieldStyle = css` const FieldStyle = css`
margin-bottom: 1rem; margin-bottom: 1rem;
@ -35,7 +36,7 @@ export const UserBlueprintCreate: NextPage = () => {
return ( return (
<div css={{ margin: "0.7rem" }}> <div css={{ margin: "0.7rem" }}>
<Formik <Formik
initialValues={{ title: "", description: "", string: "" }} initialValues={{ title: "", description: "", string: "", tags: [] }}
validate={validateCreateBlueprintForm} validate={validateCreateBlueprintForm}
onSubmit={async (values, { setSubmitting, setErrors, setStatus }) => { onSubmit={async (values, { setSubmitting, setErrors, setStatus }) => {
setStatus(""); setStatus("");
@ -57,7 +58,7 @@ export const UserBlueprintCreate: NextPage = () => {
} }
}} }}
> >
{({ isSubmitting, handleSubmit, status, values, errors }) => ( {({ isSubmitting, handleSubmit, status, values, errors, setFieldValue }) => (
<SimpleGrid <SimpleGrid
columns={2} columns={2}
gap={6} gap={6}
@ -97,14 +98,12 @@ export const UserBlueprintCreate: NextPage = () => {
<Field name="tags"> <Field name="tags">
{({ field, meta }: any) => ( {({ field, meta }: any) => (
<FormControl <FormControl id="tags" isInvalid={meta.touched && meta.error} css={FieldStyle}>
id="tags" <FormLabel>Tags</FormLabel>
// isRequired <TagsSelect
isInvalid={meta.touched && meta.error} value={field.value}
css={FieldStyle} onChange={(tags) => setFieldValue("tags", tags)}
> />
<FormLabel>Tags (coming soon)</FormLabel>
<Input type="text" {...field} disabled />
<FormErrorMessage>{meta.error}</FormErrorMessage> <FormErrorMessage>{meta.error}</FormErrorMessage>
</FormControl> </FormControl>
)} )}

View File

@ -14,35 +14,25 @@ import {
Text, Text,
Textarea, Textarea,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import MultiSelect from "react-multi-select-component";
import { chakraResponsive } from "@factorio-sites/web-utils"; import { chakraResponsive } from "@factorio-sites/web-utils";
import { import {
Blueprint,
BlueprintBook,
BlueprintPage,
getBlueprintBookById, getBlueprintBookById,
getBlueprintById, getBlueprintById,
getBlueprintPageById, getBlueprintPageById,
getBlueprintStringByHash, getBlueprintStringByHash,
} from "@factorio-sites/database"; } from "@factorio-sites/database";
import { Blueprint, BlueprintBook, BlueprintPage } from "@factorio-sites/types";
import { pageHandler } from "../../../utils/page-handler"; import { pageHandler } from "../../../utils/page-handler";
import { Panel } from "../../../components/Panel"; import { Panel } from "../../../components/Panel";
import { validateCreateBlueprintForm } from "../../../utils/validate"; import { validateCreateBlueprintForm } from "../../../utils/validate";
import { useAuth } from "../../../providers/auth"; import { useAuth } from "../../../providers/auth";
import { ImageEditor } from "../../../components/ImageEditor"; import { ImageEditor } from "../../../components/ImageEditor";
import { useFbeData } from "../../../hooks/fbe.hook"; import { TagsSelect } from "../../../components/TagsSelect";
const FieldStyle = css` const FieldStyle = css`
margin-bottom: 1rem; margin-bottom: 1rem;
`; `;
// const TAGS = [
// { value: "foo", label: "foo" },
// { value: "bar", label: "bar" },
// { value: "x", label: "x" },
// { value: "y", label: "y" },
// ];
type Selected = type Selected =
| { type: "blueprint"; data: Pick<Blueprint, "id" | "blueprint_hash">; string: string } | { type: "blueprint"; data: Pick<Blueprint, "id" | "blueprint_hash">; string: string }
| { type: "blueprint_book"; data: Pick<BlueprintBook, "id" | "blueprint_hash">; string: string }; | { type: "blueprint_book"; data: Pick<BlueprintBook, "id" | "blueprint_hash">; string: string };
@ -54,7 +44,6 @@ interface UserBlueprintProps {
export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, selected }) => { export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, selected }) => {
const auth = useAuth(); const auth = useAuth();
const router = useRouter(); const router = useRouter();
const { data } = useFbeData();
if (!auth) { if (!auth) {
router.push("/"); router.push("/");
@ -62,15 +51,6 @@ export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, sel
if (!blueprintPage) return null; if (!blueprintPage) return null;
if (!data) return null;
const TAGS = Object.keys(data.entities)
.filter((key) => !key.startsWith("factorio_logo") && !key.startsWith("crash_site"))
.map((key) => {
const item = data.entities[key];
return { value: item.name, label: item.name.replace(/_/g, " ") };
});
return ( return (
<div css={{ margin: "0.7rem" }}> <div css={{ margin: "0.7rem" }}>
<Formik <Formik
@ -78,7 +58,7 @@ export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, sel
title: blueprintPage.title, title: blueprintPage.title,
description: blueprintPage.description_markdown, description: blueprintPage.description_markdown,
string: selected.string, string: selected.string,
tags: [] as { value: string; label: string }[], tags: [] as string[],
}} }}
validate={validateCreateBlueprintForm} validate={validateCreateBlueprintForm}
onSubmit={async (values, { setSubmitting, setErrors, setStatus }) => { onSubmit={async (values, { setSubmitting, setErrors, setStatus }) => {
@ -90,7 +70,6 @@ export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, sel
body: JSON.stringify({ body: JSON.stringify({
...values, ...values,
id: blueprintPage.id, id: blueprintPage.id,
tags: values.tags.map((tag) => tag.value),
}), }),
}).then((res) => res.json()); }).then((res) => res.json());
@ -147,13 +126,9 @@ export const UserBlueprint: NextPage<UserBlueprintProps> = ({ blueprintPage, sel
{({ field, meta }: any) => ( {({ field, meta }: any) => (
<FormControl id="tags" isInvalid={meta.touched && meta.error} css={FieldStyle}> <FormControl id="tags" isInvalid={meta.touched && meta.error} css={FieldStyle}>
<FormLabel>Tags</FormLabel> <FormLabel>Tags</FormLabel>
<MultiSelect <TagsSelect
css={{ color: "black" }}
options={TAGS}
value={field.value} value={field.value}
onChange={(value: any) => setFieldValue("tags", value)} onChange={(tags) => setFieldValue("tags", tags)}
labelledBy="Select"
hasSelectAll={false}
/> />
<FormErrorMessage>{meta.error}</FormErrorMessage> <FormErrorMessage>{meta.error}</FormErrorMessage>
</FormControl> </FormControl>

View File

@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import { NextPage } from "next"; import { NextPage } from "next";
import { Button, SimpleGrid, Box } from "@chakra-ui/react";
import { Panel } from "../../components/Panel";
import Link from "next/link"; import Link from "next/link";
import { Button, SimpleGrid, Box } from "@chakra-ui/react";
import { getBlueprintPageByUserId } from "@factorio-sites/database";
import { BlueprintPage } from "@factorio-sites/types";
import { pageHandler } from "../../utils/page-handler"; import { pageHandler } from "../../utils/page-handler";
import { BlueprintPage, getBlueprintPageByUserId } from "@factorio-sites/database";
import { BlueprintLink } from "../../components/BlueprintLink"; import { BlueprintLink } from "../../components/BlueprintLink";
import { Panel } from "../../components/Panel";
interface UserBlueprintsProps { interface UserBlueprintsProps {
blueprints: BlueprintPage[]; blueprints: BlueprintPage[];
} }
@ -33,7 +33,7 @@ export const UserBlueprints: NextPage<UserBlueprintsProps> = ({ blueprints }) =>
</Box> </Box>
<Box> <Box>
{blueprints.map((bp) => ( {blueprints.map((bp) => (
<BlueprintLink key={bp.id} blueprint={bp} editLink /> <BlueprintLink key={bp.id} blueprint={bp} editLink type="row" />
))} ))}
</Box> </Box>
</Panel> </Panel>

View File

@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { getSessionByToken, init } from "@factorio-sites/database"; import { getSessionByToken, init } from "@factorio-sites/database";
import { deleteSessionToken, getSessionToken } from "@factorio-sites/node-utils"; import { deleteSessionToken, getSessionToken, jsonReplaceErrors } from "@factorio-sites/node-utils";
type Await<T> = T extends PromiseLike<infer U> ? Await<U> : T; type Await<T> = T extends PromiseLike<infer U> ? Await<U> : T;
@ -10,6 +10,19 @@ interface CustomContext {
useragent: string; useragent: string;
} }
export class ApiError extends Error {
constructor(
public status: number,
public message: string,
public data?: Record<string, unknown>
) {
super(message);
}
toJSON() {
return { ...this.data, status: this.status, message: this.message };
}
}
export const apiHandler = ( export const apiHandler = (
fn: (req: NextApiRequest, res: NextApiResponse, ctx: CustomContext) => Promise<any> fn: (req: NextApiRequest, res: NextApiResponse, ctx: CustomContext) => Promise<any>
) => async (req: NextApiRequest, res: NextApiResponse) => { ) => async (req: NextApiRequest, res: NextApiResponse) => {
@ -25,5 +38,13 @@ export const apiHandler = (
deleteSessionToken(res); deleteSessionToken(res);
} }
return fn(req, res, { session, ip, useragent }); return fn(req, res, { session, ip, useragent }).catch((error) => {
if (error instanceof ApiError) {
res.status(error.status).send(JSON.stringify({ error }));
} else if (process.env.NODE_ENV === "development") {
res.status(500).send(JSON.stringify({ error }, jsonReplaceErrors));
} else {
res.status(500).json({ error: "Internal server error" });
}
});
}; };

View File

@ -1,27 +1,35 @@
FROM node:14-slim as builder FROM node:14-slim as builder
RUN apt-get -qy update && apt-get -qy install openssl
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package.json . COPY package.json .
COPY yarn.lock . COPY yarn.lock .
COPY fbe-editor-v1.0.0.tgz . COPY yalc.lock .
COPY .yalc ./.yalc/
RUN yarn RUN yarn
COPY . . COPY . .
RUN yarn run db-gen
RUN yarn nx build blueprints RUN yarn nx build blueprints
FROM node:14-slim FROM node:14-slim
RUN apt-get -qy update && apt-get -qy install openssl
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY apps/blueprints/prod.package.json ./package.json COPY apps/blueprints/prod.package.json ./package.json
COPY yarn.lock . COPY yarn.lock .
COPY fbe-editor-v1.0.0.tgz . COPY yalc.lock .
COPY .yalc ./.yalc/
RUN yarn install --production RUN yarn install --production
COPY --from=builder /usr/src/app/node_modules/.prisma ./node_modules/.prisma/
COPY --from=builder /usr/src/app/dist/apps/blueprints . COPY --from=builder /usr/src/app/dist/apps/blueprints .
CMD ["yarn", "next", "start"] CMD ["yarn", "next", "start"]

View File

@ -7,5 +7,6 @@ module.exports = {
"<rootDir>/libs/utils", "<rootDir>/libs/utils",
"<rootDir>/libs/common-utils", "<rootDir>/libs/common-utils",
"<rootDir>/libs/web-utils", "<rootDir>/libs/web-utils",
"<rootDir>/libs/types",
], ],
}; };

View File

@ -2,4 +2,3 @@ export * from "./lib/postgres/database";
export * from "./lib/data"; export * from "./lib/data";
export * from "./lib/gcp-pubsub"; export * from "./lib/gcp-pubsub";
export * from "./lib/gcp-storage"; export * from "./lib/gcp-storage";
export * from "./lib/types";

View File

@ -3,7 +3,7 @@ import { encodeBlueprint, hashString } from "@factorio-sites/node-utils";
import { blueprint as BlueprintModel } from "@prisma/client"; import { blueprint as BlueprintModel } from "@prisma/client";
import { saveBlueprintString } from "../gcp-storage"; import { saveBlueprintString } from "../gcp-storage";
import { prisma } from "../postgres/database"; import { prisma } from "../postgres/database";
import { Blueprint } from "../types"; import { Blueprint } from "@factorio-sites/types";
// const blueprintImageRequestTopic = getBlueprintImageRequestTopic(); // const blueprintImageRequestTopic = getBlueprintImageRequestTopic();
@ -36,21 +36,19 @@ export async function createBlueprint(
created_at?: number; created_at?: number;
updated_at?: number; updated_at?: number;
} }
) { ): Promise<Blueprint> {
const string = await encodeBlueprint({ blueprint }); const string = await encodeBlueprint({ blueprint });
const blueprint_hash = hashString(string); const blueprint_hash = hashString(string);
const image_hash = hashString(getBlueprintContentForImageHash(blueprint)); const image_hash = hashString(getBlueprintContentForImageHash(blueprint));
const exists = await getBlueprintByHash(blueprint_hash); const exists = await getBlueprintByHash(blueprint_hash);
if (exists) { if (exists) {
return { insertedId: exists.id }; return exists;
} }
// Write string to google storage // Write string to google storage
await saveBlueprintString(blueprint_hash, string); await saveBlueprintString(blueprint_hash, string);
// Write blueprint details to datastore
const result = await prisma.blueprint.create({ const result = await prisma.blueprint.create({
data: { data: {
label: blueprint.label, label: blueprint.label,
@ -71,5 +69,5 @@ export async function createBlueprint(
// blueprintId: result.id, // blueprintId: result.id,
// }); // });
return { insertedId: result.id }; return mapBlueprintInstanceToEntry(result);
} }

View File

@ -3,7 +3,7 @@ import { encodeBlueprint, hashString } from "@factorio-sites/node-utils";
import { blueprint_book } from "@prisma/client"; import { blueprint_book } from "@prisma/client";
import { saveBlueprintString } from "../gcp-storage"; import { saveBlueprintString } from "../gcp-storage";
import { prisma } from "../postgres/database"; import { prisma } from "../postgres/database";
import { BlueprintBook, ChildTree } from "../types"; import { BlueprintBook, ChildTree } from "@factorio-sites/types";
import { createBlueprint } from "./blueprint"; import { createBlueprint } from "./blueprint";
const mapBlueprintBookEntityToObject = (entity: blueprint_book): BlueprintBook => ({ const mapBlueprintBookEntityToObject = (entity: blueprint_book): BlueprintBook => ({
@ -34,7 +34,7 @@ export async function createBlueprintBook(
created_at?: number; created_at?: number;
updated_at?: number; updated_at?: number;
} }
): Promise<{ insertedId: string; child_tree: ChildTree }> { ): Promise<BlueprintBook> {
const string = await encodeBlueprint({ blueprint_book: blueprintBook }); const string = await encodeBlueprint({ blueprint_book: blueprintBook });
const blueprint_hash = hashString(string); const blueprint_hash = hashString(string);
@ -42,7 +42,7 @@ export async function createBlueprintBook(
if (exists) { if (exists) {
const book = await getBlueprintBookById(exists.id); const book = await getBlueprintBookById(exists.id);
if (!book) throw Error("this is impossible, just pleasing typescript"); if (!book) throw Error("this is impossible, just pleasing typescript");
return { insertedId: exists.id, child_tree: book.child_tree }; return exists;
} }
// Write string to google storage // Write string to google storage
@ -59,19 +59,19 @@ export async function createBlueprintBook(
const result = await createBlueprint(blueprint.blueprint, extraInfo); const result = await createBlueprint(blueprint.blueprint, extraInfo);
child_tree.push({ child_tree.push({
type: "blueprint", type: "blueprint",
id: result.insertedId, id: result.id,
name: blueprint.blueprint.label, name: blueprint.blueprint.label,
}); });
blueprint_ids.push(result.insertedId); blueprint_ids.push(result.id);
} else if (blueprint.blueprint_book) { } else if (blueprint.blueprint_book) {
const result = await createBlueprintBook(blueprint.blueprint_book, extraInfo); const result = await createBlueprintBook(blueprint.blueprint_book, extraInfo);
child_tree.push({ child_tree.push({
type: "blueprint_book", type: "blueprint_book",
id: result.insertedId, id: result.id,
name: blueprint.blueprint_book.label, name: blueprint.blueprint_book.label,
children: result.child_tree, children: result.child_tree,
}); });
blueprint_book_ids.push(result.insertedId); blueprint_book_ids.push(result.id);
} }
} }
@ -84,10 +84,20 @@ export async function createBlueprintBook(
child_tree: child_tree as any, child_tree: child_tree as any,
updated_at: extraInfo.updated_at ? new Date(extraInfo.updated_at * 1000) : new Date(), updated_at: extraInfo.updated_at ? new Date(extraInfo.updated_at * 1000) : new Date(),
created_at: extraInfo.created_at ? new Date(extraInfo.created_at * 1000) : new Date(), created_at: extraInfo.created_at ? new Date(extraInfo.created_at * 1000) : new Date(),
blueprint_books: {
connect: blueprint_book_ids.map((id) => ({
id,
})),
},
blueprints: {
connect: blueprint_ids.map((id) => ({
id,
})),
},
}, },
}); });
console.log(`Created Blueprint book ${result.id}`); console.log(`Created Blueprint book ${result.id}`);
return { insertedId: result.id, child_tree }; return mapBlueprintBookEntityToObject(result);
} }

View File

@ -1,8 +1,8 @@
import { blueprint_page } from "@prisma/client"; import { blueprint_page } from "@prisma/client";
import { join, raw, sqltag } from "@prisma/client/runtime"; import { join, raw, sqltag } from "@prisma/client/runtime";
import { getBlueprintImageRequestTopic } from "../gcp-pubsub";
import { prisma } from "../postgres/database"; import { prisma } from "../postgres/database";
import { BlueprintPage } from "../types"; import { BlueprintPage } from "@factorio-sites/types";
const mapBlueprintPageEntityToObject = (entity: blueprint_page): BlueprintPage => ({ const mapBlueprintPageEntityToObject = (entity: blueprint_page): BlueprintPage => ({
id: entity.id, id: entity.id,
@ -11,6 +11,7 @@ const mapBlueprintPageEntityToObject = (entity: blueprint_page): BlueprintPage =
title: entity.title, title: entity.title,
description_markdown: entity.description_markdown || "", description_markdown: entity.description_markdown || "",
tags: entity.tags, tags: entity.tags,
image_hash: entity.image_hash,
created_at: entity.created_at && entity.created_at.getTime() / 1000, created_at: entity.created_at && entity.created_at.getTime() / 1000,
updated_at: entity.updated_at && entity.updated_at.getTime() / 1000, updated_at: entity.updated_at && entity.updated_at.getTime() / 1000,
factorioprints_id: entity.factorioprints_id ?? null, factorioprints_id: entity.factorioprints_id ?? null,
@ -78,30 +79,44 @@ export async function searchBlueprintPages({
export async function createBlueprintPage( export async function createBlueprintPage(
type: "blueprint" | "blueprint_book", type: "blueprint" | "blueprint_book",
targetId: string, targetId: string,
extraInfo: { data: {
title: string; title: string;
user_id: string | null; user_id: string | null;
description_markdown: string; description_markdown: string;
tags?: string[]; tags?: string[];
image_hash: string;
created_at?: number; created_at?: number;
updated_at?: number; updated_at?: number;
firstBlueprintId?: string;
factorioprints_id?: string; factorioprints_id?: string;
} }
) { ) {
const page = await prisma.blueprint_page.create({ const page = await prisma.blueprint_page.create({
data: { data: {
user_id: extraInfo.user_id, user_id: data.user_id,
title: extraInfo.title, title: data.title,
description_markdown: extraInfo.description_markdown, description_markdown: data.description_markdown,
factorioprints_id: extraInfo.factorioprints_id, factorioprints_id: data.factorioprints_id,
blueprint_id: type === "blueprint" ? targetId : undefined, blueprint_id: type === "blueprint" ? targetId : undefined,
blueprint_book_id: type === "blueprint_book" ? targetId : undefined, blueprint_book_id: type === "blueprint_book" ? targetId : undefined,
tags: extraInfo.tags ? extraInfo.tags : [], tags: data.tags ? data.tags : [],
updated_at: extraInfo.updated_at ? new Date(extraInfo.updated_at * 1000) : new Date(), image_hash: data.image_hash,
created_at: extraInfo.created_at ? new Date(extraInfo.created_at * 1000) : new Date(), updated_at: data.updated_at ? new Date(data.updated_at * 1000) : new Date(),
created_at: data.created_at ? new Date(data.created_at * 1000) : new Date(),
}, },
}); });
const blueprintImageRequestTopic = getBlueprintImageRequestTopic();
if (type === "blueprint") {
blueprintImageRequestTopic.publishJSON({
blueprintId: targetId,
});
} else if (data.firstBlueprintId) {
blueprintImageRequestTopic.publishJSON({
blueprintId: data.firstBlueprintId,
});
}
console.log(`Created Blueprint Page`); console.log(`Created Blueprint Page`);
return page; return page;
} }

View File

@ -38,12 +38,20 @@ export async function saveBlueprintFromFactorioprints(
if (parsed.data.blueprint) { if (parsed.data.blueprint) {
console.log("string has one blueprint..."); console.log("string has one blueprint...");
const { insertedId } = await createBlueprint(parsed.data.blueprint, extraInfo); const result = await createBlueprint(parsed.data.blueprint, extraInfo);
await createBlueprintPage("blueprint", insertedId, extraInfoPage); await createBlueprintPage("blueprint", result.id, {
...extraInfoPage,
firstBlueprintId: result.id,
image_hash: "",
});
} else if (parsed.data.blueprint_book) { } else if (parsed.data.blueprint_book) {
console.log("string has a blueprint book..."); console.log("string has a blueprint book...");
const { insertedId } = await createBlueprintBook(parsed.data.blueprint_book, extraInfo); const result = await createBlueprintBook(parsed.data.blueprint_book, extraInfo);
await createBlueprintPage("blueprint_book", insertedId, extraInfoPage); await createBlueprintPage("blueprint_book", result.id, {
...extraInfoPage,
firstBlueprintId: undefined,
image_hash: "",
});
} }
return true; return true;

View File

@ -1,5 +1,4 @@
import { PubSub, Message } from "@google-cloud/pubsub"; import { PubSub, Message } from "@google-cloud/pubsub";
// export { Message } from "@google-cloud/pubsub";
const pubsub = new PubSub(); const pubsub = new PubSub();

View File

@ -22,13 +22,30 @@ export async function saveBlueprintString(hash: string, content: string) {
* BlueprintImage * BlueprintImage
*/ */
export async function saveBlueprintImage(hash: string, image: Buffer): Promise<void> { type sizeType = "original" | "300";
return IMAGE_BUCKET.file(`${hash}.webp`).save(image, {
export async function saveBlueprintImage(
hash: string,
image: Buffer,
type: sizeType = "original"
): Promise<void> {
return IMAGE_BUCKET.file(`${type}/${hash}.webp`).save(image, {
contentType: "image/webp", contentType: "image/webp",
}); });
} }
export async function hasBlueprintImage(hash: string): Promise<boolean> { export async function hasBlueprintImage(
const [result] = await IMAGE_BUCKET.file(`${hash}.webp`).exists(); hash: string,
type: sizeType = "original"
): Promise<boolean> {
const [result] = await IMAGE_BUCKET.file(`${type}/${hash}.webp`).exists();
return result;
}
export async function getBlueprintByImageHash(
hash: string,
type: sizeType = "original"
): Promise<Buffer> {
const [result] = await IMAGE_BUCKET.file(`${type}/${hash}.webp`).download();
return result; return result;
} }

View File

@ -38,4 +38,7 @@ const promise = _init()
console.log("Database failed to init!", reason); console.log("Database failed to init!", reason);
}); });
export const init = async () => promise; export const init = async () => {
await promise;
await prisma.$connect();
};

View File

@ -1,64 +0,0 @@
import { DataTypes, UUIDV4, Optional, Model, Sequelize } from "sequelize";
interface BlueprintAttributes {
id: string;
label?: string;
description?: string;
game_version?: string;
blueprint_hash: string;
image_hash: string;
image_version: number;
tags: string[];
created_at: Date;
updated_at: Date;
}
export interface BlueprintInstance
extends Model<
Omit<BlueprintAttributes, "created_at" | "updated_at">,
Optional<BlueprintAttributes, "id" | "created_at" | "updated_at">
>,
BlueprintAttributes {}
export const getBlueprintModel = (sequelize: Sequelize) => {
return sequelize.define<BlueprintInstance>(
"blueprint",
{
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: UUIDV4,
},
label: {
type: DataTypes.STRING,
},
description: {
type: DataTypes.TEXT,
},
game_version: {
type: DataTypes.STRING,
},
blueprint_hash: {
type: DataTypes.STRING(40),
unique: true,
allowNull: false,
},
image_hash: {
type: DataTypes.STRING(40),
allowNull: false,
},
image_version: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1,
},
tags: {
type: DataTypes.ARRAY(DataTypes.STRING),
set(value: string[]) {
this.setDataValue("tags", Array.isArray(value) ? value : []);
},
},
},
{}
);
};

View File

@ -1,52 +0,0 @@
import { DataTypes, UUIDV4, Optional, Model, Sequelize } from "sequelize";
import { ChildTree } from "../../types";
interface BlueprintBookAttributes {
id: string;
label: string;
description?: string;
child_tree: ChildTree;
blueprint_hash: string;
is_modded: boolean;
created_at: Date;
updated_at: Date;
}
export interface BlueprintBookInstance
extends Model<
Omit<BlueprintBookAttributes, "created_at" | "updated_at">,
Optional<BlueprintBookAttributes, "id" | "created_at" | "updated_at">
>,
BlueprintBookAttributes {}
export const getBlueprintBookModel = (sequelize: Sequelize) => {
return sequelize.define<BlueprintBookInstance>(
"blueprint_book",
{
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: UUIDV4,
},
label: {
type: DataTypes.STRING,
},
description: {
type: DataTypes.TEXT,
},
child_tree: {
type: DataTypes.JSON,
allowNull: false,
},
blueprint_hash: {
type: DataTypes.STRING(40),
allowNull: false,
},
is_modded: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
},
{}
);
};

View File

@ -1,73 +0,0 @@
import { DataTypes, UUIDV4, Optional, Model, Sequelize } from "sequelize";
interface BlueprintPageAttributes {
id: string;
user_id: string | null;
blueprint_id?: string;
blueprint_book_id?: string;
title: string;
description_markdown: string;
tags: string[];
factorioprints_id?: string;
created_at: Date;
updated_at: Date;
}
export interface BlueprintPageInstance
extends Model<
Omit<BlueprintPageAttributes, "created_at" | "updated_at">,
Optional<BlueprintPageAttributes, "id" | "created_at" | "updated_at">
>,
BlueprintPageAttributes {}
export const getBlueprintPageModel = (sequelize: Sequelize) => {
return sequelize.define<BlueprintPageInstance>(
"blueprint_page",
{
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: UUIDV4,
},
user_id: {
type: DataTypes.UUID,
},
blueprint_id: {
type: DataTypes.UUID,
unique: true,
},
blueprint_book_id: {
type: DataTypes.UUID,
unique: true,
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
description_markdown: {
type: DataTypes.TEXT,
},
tags: {
type: DataTypes.ARRAY(DataTypes.STRING),
set(value: string[]) {
this.setDataValue("tags", Array.isArray(value) ? value : []);
},
},
factorioprints_id: {
type: DataTypes.STRING,
},
},
{ validate:{
blueprintOrBook() {
if (!this.blueprint_id && !this.blueprint_book_id) {
throw new Error('Must have either a blueprint_id or a blueprint_book_id');
}
},
externalOrInternal() {
if (!this.user_id && !this.factorioprints_id) {
throw new Error('Must have either a user_id or a factorioprints_id');
}
}
}}
);
};

View File

@ -1,59 +0,0 @@
import { DataTypes, UUIDV4, Optional, Model, Sequelize } from "sequelize";
import * as crypto from "crypto";
interface SessionAttributes {
id: string;
user_id: string;
session_token: string;
useragent: string;
ip: string;
last_used: Date;
created_at: Date;
updated_at: Date;
}
export interface SessionInstance
extends Model<
Omit<SessionAttributes, "created_at" | "updated_at">,
Optional<
SessionAttributes,
"id" | "session_token" | "last_used" | "created_at" | "updated_at"
>
>,
SessionAttributes {}
export const getSessionModel = (sequelize: Sequelize) => {
return sequelize.define<SessionInstance>(
"session",
{
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: UUIDV4,
},
user_id: {
type: DataTypes.UUID,
allowNull: false,
},
session_token: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: () => crypto.randomBytes(30).toString("base64"),
},
useragent: {
type: DataTypes.STRING,
allowNull: false,
},
ip: {
type: DataTypes.STRING,
allowNull: false,
},
last_used: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
},
{}
);
};

View File

@ -1,104 +0,0 @@
import { DataTypes, UUIDV4, Optional, Model, Sequelize } from "sequelize";
interface UserAttributes {
id: string;
email: string | null;
username: string;
password: string;
role: "user" | "moderator" | "admin";
steam_id: string | null;
password_reset_token: string | null;
password_reset_at: Date | null;
last_password_change: Date;
last_login_at: Date | null;
last_login_ip: string;
email_validated: boolean;
email_validate_token: string | null;
created_at: Date;
updated_at: Date;
}
export interface UserInstance
extends Model<
Omit<UserAttributes, "created_at" | "updated_at">,
Optional<
UserAttributes,
| "id"
| "role"
| "steam_id"
| "email"
| "password"
| "password_reset_token"
| "password_reset_at"
| "last_password_change"
| "last_login_at"
| "email_validated"
| "email_validate_token"
| "created_at"
| "updated_at"
>
>,
UserAttributes {}
export const getUsertModel = (sequelize: Sequelize) => {
return sequelize.define<UserInstance>(
"user",
{
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: UUIDV4,
},
email: {
type: DataTypes.STRING,
unique: true,
validate: { isEmail: true },
set(val: string) {
this.setDataValue("email", val.toLowerCase().trim());
},
},
username: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
},
role: {
type: DataTypes.ENUM("user", "moderator", "admin"),
defaultValue: "user",
},
steam_id: {
type: DataTypes.STRING,
unique: true,
},
password: {
type: DataTypes.STRING,
},
password_reset_token: {
type: DataTypes.UUID,
},
password_reset_at: {
type: DataTypes.DATE,
},
last_password_change: {
type: DataTypes.DATE,
},
last_login_at: {
type: DataTypes.DATE,
},
last_login_ip: {
type: DataTypes.STRING,
allowNull: false,
validate: { isIP: true },
},
email_validated: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
},
email_validate_token: {
type: DataTypes.UUID,
},
},
{}
);
};

View File

@ -1,9 +1,10 @@
import { IncomingMessage } from "http";
import * as crypto from "crypto"; import * as crypto from "crypto";
import * as pako from "pako"; import * as pako from "pako";
import * as cookie from "cookie"; import * as cookie from "cookie";
import { BlueprintStringData } from "@factorio-sites/common-utils";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { IncomingMessage } from "http"; import { BlueprintStringData } from "@factorio-sites/common-utils";
import { ChildTree } from "@factorio-sites/types";
export const parseBlueprintString = async ( export const parseBlueprintString = async (
string: string string: string
@ -67,3 +68,33 @@ export const deleteSessionToken = (res: NextApiResponse) => {
}) })
); );
}; };
export function getFirstBlueprintFromChildTree(child_tree: ChildTree): string {
// First try flat search
const result = child_tree.find((child) => child.type === "blueprint");
if (result) return result.id;
// Recusrive search
let blueprint_id: string | null = null;
child_tree.forEach((child) => {
if (child.type === "blueprint_book") {
const bp = getFirstBlueprintFromChildTree([child]);
if (bp) blueprint_id = bp;
}
});
if (!blueprint_id) throw Error("Failed to find blueprint id in child_tree");
return blueprint_id;
}
export function jsonReplaceErrors(_: string, value: unknown) {
if (value instanceof Error) {
return Object.getOwnPropertyNames(value).reduce((error, key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error[key] = (value as any)[key];
return error;
}, {} as Record<string, unknown>);
}
return value;
}

View File

@ -0,0 +1,5 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"rules": {}
}

7
libs/types/README.md Normal file
View File

@ -0,0 +1,7 @@
# types
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test types` to execute the unit tests via [Jest](https://jestjs.io).

14
libs/types/jest.config.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
displayName: "types",
preset: "../../jest.preset.js",
globals: {
"ts-jest": {
tsConfig: "<rootDir>/tsconfig.spec.json",
},
},
transform: {
"^.+\\.[tj]sx?$": "ts-jest",
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
coverageDirectory: "../../coverage/libs/types",
};

1
libs/types/src/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./lib/types";

View File

@ -47,6 +47,7 @@ export interface BlueprintPage {
title: string; title: string;
description_markdown: string; description_markdown: string;
tags: string[]; tags: string[];
image_hash: string;
created_at: number; created_at: number;
updated_at: number; updated_at: number;
factorioprints_id: string | null; factorioprints_id: string | null;

13
libs/types/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["**/*.spec.ts"],
"include": ["**/*.ts"]
}

View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js", "**/*.spec.jsx", "**/*.d.ts"]
}

47
nx.json
View File

@ -1,14 +1,9 @@
{ {
"npmScope": "factorio-sites", "npmScope": "factorio-sites",
"affected": { "affected": { "defaultBase": "master" },
"defaultBase": "master"
},
"implicitDependencies": { "implicitDependencies": {
"workspace.json": "*", "workspace.json": "*",
"package.json": { "package.json": { "dependencies": "*", "devDependencies": "*" },
"dependencies": "*",
"devDependencies": "*"
},
"tsconfig.base.json": "*", "tsconfig.base.json": "*",
"tslint.json": "*", "tslint.json": "*",
".eslintrc.json": "*", ".eslintrc.json": "*",
@ -17,36 +12,18 @@
"tasksRunnerOptions": { "tasksRunnerOptions": {
"default": { "default": {
"runner": "@nrwl/workspace/tasks-runners/default", "runner": "@nrwl/workspace/tasks-runners/default",
"options": { "options": { "cacheableOperations": ["build", "lint", "test", "e2e"] }
"cacheableOperations": ["build", "lint", "test", "e2e"]
}
} }
}, },
"projects": { "projects": {
"blueprints": { "blueprints": { "tags": [] },
"tags": [] "blueprints-e2e": { "tags": [], "implicitDependencies": ["blueprints"] },
}, "blueprint-image-function": { "tags": [] },
"blueprints-e2e": { "factorioprints-scraper": { "tags": [] },
"tags": [], "database": { "tags": [] },
"implicitDependencies": ["blueprints"] "node-utils": { "tags": [] },
}, "common-utils": { "tags": [] },
"blueprint-image-function": { "web-utils": { "tags": [] },
"tags": [] "types": { "tags": [] }
},
"factorioprints-scraper": {
"tags": []
},
"database": {
"tags": []
},
"node-utils": {
"tags": []
},
"common-utils": {
"tags": []
},
"web-utils": {
"tags": []
}
} }
} }

View File

@ -20,7 +20,8 @@
"@factorio-sites/database": ["libs/database/src/index.ts"], "@factorio-sites/database": ["libs/database/src/index.ts"],
"@factorio-sites/node-utils": ["libs/node-utils/src/index.ts"], "@factorio-sites/node-utils": ["libs/node-utils/src/index.ts"],
"@factorio-sites/common-utils": ["libs/common-utils/src/index.ts"], "@factorio-sites/common-utils": ["libs/common-utils/src/index.ts"],
"@factorio-sites/web-utils": ["libs/web-utils/src/index.ts"] "@factorio-sites/web-utils": ["libs/web-utils/src/index.ts"],
"@factorio-sites/types": ["libs/types/src/index.ts"]
} }
}, },
"exclude": ["node_modules", "tmp"] "exclude": ["node_modules", "tmp"]

View File

@ -260,6 +260,27 @@
} }
} }
} }
},
"types": {
"root": "libs/types",
"sourceRoot": "libs/types/src",
"projectType": "library",
"architect": {
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": ["libs/types/**/*.ts"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"outputs": ["coverage/libs/types"],
"options": {
"jestConfig": "libs/types/jest.config.js",
"passWithNoTests": true
}
}
}
} }
}, },
"cli": { "cli": {

View File

@ -2,7 +2,7 @@
"version": "v1", "version": "v1",
"packages": { "packages": {
"@fbe/editor": { "@fbe/editor": {
"signature": "77d812874aacc6417c4ac5324c11cddf", "signature": "e5aba5c824b95e84453566186e48efe8",
"file": true, "file": true,
"replaced": "./fbe-editor-v1.0.0.tgz" "replaced": "./fbe-editor-v1.0.0.tgz"
} }