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": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
"recommendations": ["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";
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): Promise<Buffer> => {
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;
export const optimise = async (image: Buffer, max_dimention = 5000): Promise<Buffer> => {
const sharp_image = await sharp(image)
.resize({
width: max_dimention,
height: max_dimention,
fit: sharp.fit.inside,
})
.then((image) => image.webp({ lossless: true }));
} else {
sharp_image = sharp_image.webp({ lossless: true });
}
.webp({ lossless: true })
.toBuffer();
const min_image = await sharp_image.toBuffer();
console.log(
JSON.stringify({
input_size: `${Math.round(image.byteLength / 1024) / 1000}mb`,
output_size: `${Math.round(sharp_image.byteLength / 1024) / 1000}mb`,
})
);
console.log({
input_size_mb: image.byteLength / 1024_000,
output_size_mb: min_image.byteLength / 1024_000,
});
return min_image;
return sharp_image;
};

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

@ -25,6 +25,7 @@ model blueprint {
created_at DateTime @default(now())
updated_at DateTime @updatedAt
blueprint_page blueprint_page?
blueprint_books blueprint_book[]
}
model blueprint_book {
@ -37,20 +38,9 @@ model blueprint_book {
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])
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

View File

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

View File

@ -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<BlueprintLinkProps> = ({ blueprint, editLink }) => (
<div css={linkStyles}>
export const BlueprintLink: React.FC<BlueprintLinkProps> = ({
blueprint,
editLink,
type = "tile",
}) => {
const [imageError, setImageError] = useState(false);
const onImageError = (error: unknown) => {
console.log(error);
setImageError(true);
};
return (
<div css={linkStyles} className={type}>
<Link
href={editLink ? `/user/blueprint/${blueprint.id}` : `/blueprint/${blueprint.id}`}
passHref
>
<a>
<Box css={{ display: "flex", justifyContent: "space-between" }}>
<Text>{blueprint.title}</Text>
<Box css={{ display: "flex" }}>
<Text css={{ display: "flex", alignItems: "center", marginRight: "2rem" }}>
<MdFavorite css={{ marginRight: "0.5rem" }} />
{blueprint.favorite_count}
<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>
</a>
</Link>
</div>
);
};

View File

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

View File

@ -70,7 +70,7 @@ export const ImageEditor: React.FC<{ string: string }> = ({ string }) => {
return (
<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> */}
</div>
);

View File

@ -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) {

View File

@ -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) {

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,
getBlueprintStringByHash,
hasBlueprintImage,
Blueprint,
} from "@factorio-sites/database";
import { Blueprint } from "@factorio-sites/types";
const DISABLED = true;
const getOneMessage = async (): Promise<Blueprint> => {
const topic = getBlueprintImageRequestTopic();
@ -43,6 +45,8 @@ const getOneMessage = async (): Promise<Blueprint> => {
};
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 &&

View File

@ -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(`
<html><head>
<meta http-equiv="refresh" content="0;url=${process.env.BASE_URL}" />
</head></html>
`);
}
// 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(`
<html><head>
<meta http-equiv="refresh" content="0;url=${process.env.BASE_URL}" />
</head></html>
`);
};
export default handler;

View File

@ -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<Blueprint, "id" | "blueprint_hash" | "image_hash"> }
| { type: "blueprint_book"; data: Pick<BlueprintBook, "id" | "blueprint_hash"> };
| { type: "blueprint"; data: Pick<Blueprint, "id" | "blueprint_hash" | "image_hash" | "label"> }
| { type: "blueprint_book"; data: Pick<BlueprintBook, "id" | "blueprint_hash" | "label"> };
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<IndexProps> = ({
image_exists,
selected,
blueprint,
blueprint_book,
@ -90,7 +86,6 @@ export const Index: NextPage<IndexProps> = ({
useEffect(() => {
console.log({
image_exists,
selected,
blueprint,
blueprint_book,
@ -149,9 +144,6 @@ export const Index: NextPage<IndexProps> = ({
gridColumn="1"
>
{blueprint_book ? (
<>
<div>This string contains a blueprint book </div>
<br />
<div css={{ maxHeight: "400px", overflow: "auto" }}>
<BookChildTree
child_tree={[
@ -166,7 +158,6 @@ export const Index: NextPage<IndexProps> = ({
selected_id={selected.data.id}
/>
</div>
</>
) : blueprint ? (
<Markdown>{blueprint_page.description_markdown}</Markdown>
) : null}
@ -216,12 +207,10 @@ export const Index: NextPage<IndexProps> = ({
{selected.type === "blueprint" && data?.blueprint && (
<Panel
title={
(
<span>
Entities for{" "}
{data.blueprint.label ? BBCode.toReact(data.blueprint.label) : "blueprint"}
</span>
) as any
}
gridColumn={chakraResponsive({ mobile: "1", desktop: "1 / span 2" })}
>
@ -257,7 +246,10 @@ export const Index: NextPage<IndexProps> = ({
</StyledTable>
</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" />}
<textarea
@ -370,6 +362,7 @@ export const getServerSideProps = pageHandler(async (context, { session }) => {
id: selected_blueprint.id,
blueprint_hash: selected_blueprint.blueprint_hash,
image_hash: selected_blueprint.image_hash,
label: selected_blueprint.label,
},
};
} else if (selected_blueprint_book) {
@ -378,20 +371,17 @@ export const getServerSideProps = pageHandler(async (context, { session }) => {
data: {
id: selected_blueprint_book.id,
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
? !!(await isBlueprintPageUserFavorite(session.user.id, blueprint_page.id))
: false;
return {
props: {
image_exists: false,
blueprint,
blueprint_book,
selected,

View File

@ -1,12 +1,13 @@
import React from "react";
import { NextPage, NextPageContext } from "next";
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 { BlueprintPage } from "@factorio-sites/types";
import { Panel } from "../components/Panel";
import { Pagination } from "../components/Pagination";
import { useRouterQueryToHref } from "../hooks/query.hook";
import { useRouter } from "next/router";
import { BlueprintLink } from "../components/BlueprintLink";
import { TagsSelect } from "../components/TagsSelect";
import { queryValueAsArray } from "../utils/query.utils";
@ -67,10 +68,12 @@ export const Index: NextPage<IndexProps> = ({
</Box>
)}
</Box>
<Box>
<Box css={{ display: "flex" }}>
{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} />
</Box>
</Panel>

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next";
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;
@ -10,6 +10,19 @@ interface CustomContext {
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 = (
fn: (req: NextApiRequest, res: NextApiResponse, ctx: CustomContext) => Promise<any>
) => async (req: NextApiRequest, res: NextApiResponse) => {
@ -25,5 +38,13 @@ export const apiHandler = (
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
RUN apt-get -qy update && apt-get -qy install openssl
WORKDIR /usr/src/app
COPY package.json .
COPY yarn.lock .
COPY fbe-editor-v1.0.0.tgz .
COPY yalc.lock .
COPY .yalc ./.yalc/
RUN yarn
COPY . .
RUN yarn run db-gen
RUN yarn nx build blueprints
FROM node:14-slim
RUN apt-get -qy update && apt-get -qy install openssl
WORKDIR /usr/src/app
COPY apps/blueprints/prod.package.json ./package.json
COPY yarn.lock .
COPY fbe-editor-v1.0.0.tgz .
COPY yalc.lock .
COPY .yalc ./.yalc/
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 .
CMD ["yarn", "next", "start"]

View File

@ -7,5 +7,6 @@ module.exports = {
"<rootDir>/libs/utils",
"<rootDir>/libs/common-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/gcp-pubsub";
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 { saveBlueprintString } from "../gcp-storage";
import { prisma } from "../postgres/database";
import { Blueprint } from "../types";
import { Blueprint } from "@factorio-sites/types";
// const blueprintImageRequestTopic = getBlueprintImageRequestTopic();
@ -36,21 +36,19 @@ export async function createBlueprint(
created_at?: number;
updated_at?: number;
}
) {
): Promise<Blueprint> {
const string = await encodeBlueprint({ blueprint });
const blueprint_hash = hashString(string);
const image_hash = hashString(getBlueprintContentForImageHash(blueprint));
const exists = await getBlueprintByHash(blueprint_hash);
if (exists) {
return { insertedId: exists.id };
return exists;
}
// Write string to google storage
await saveBlueprintString(blueprint_hash, string);
// Write blueprint details to datastore
const result = await prisma.blueprint.create({
data: {
label: blueprint.label,
@ -71,5 +69,5 @@ export async function createBlueprint(
// 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 { saveBlueprintString } from "../gcp-storage";
import { prisma } from "../postgres/database";
import { BlueprintBook, ChildTree } from "../types";
import { BlueprintBook, ChildTree } from "@factorio-sites/types";
import { createBlueprint } from "./blueprint";
const mapBlueprintBookEntityToObject = (entity: blueprint_book): BlueprintBook => ({
@ -34,7 +34,7 @@ export async function createBlueprintBook(
created_at?: number;
updated_at?: number;
}
): Promise<{ insertedId: string; child_tree: ChildTree }> {
): Promise<BlueprintBook> {
const string = await encodeBlueprint({ blueprint_book: blueprintBook });
const blueprint_hash = hashString(string);
@ -42,7 +42,7 @@ export async function createBlueprintBook(
if (exists) {
const book = await getBlueprintBookById(exists.id);
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
@ -59,19 +59,19 @@ export async function createBlueprintBook(
const result = await createBlueprint(blueprint.blueprint, extraInfo);
child_tree.push({
type: "blueprint",
id: result.insertedId,
id: result.id,
name: blueprint.blueprint.label,
});
blueprint_ids.push(result.insertedId);
blueprint_ids.push(result.id);
} else if (blueprint.blueprint_book) {
const result = await createBlueprintBook(blueprint.blueprint_book, extraInfo);
child_tree.push({
type: "blueprint_book",
id: result.insertedId,
id: result.id,
name: blueprint.blueprint_book.label,
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,
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(),
blueprint_books: {
connect: blueprint_book_ids.map((id) => ({
id,
})),
},
blueprints: {
connect: blueprint_ids.map((id) => ({
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 { join, raw, sqltag } from "@prisma/client/runtime";
import { getBlueprintImageRequestTopic } from "../gcp-pubsub";
import { prisma } from "../postgres/database";
import { BlueprintPage } from "../types";
import { BlueprintPage } from "@factorio-sites/types";
const mapBlueprintPageEntityToObject = (entity: blueprint_page): BlueprintPage => ({
id: entity.id,
@ -11,6 +11,7 @@ const mapBlueprintPageEntityToObject = (entity: blueprint_page): BlueprintPage =
title: entity.title,
description_markdown: entity.description_markdown || "",
tags: entity.tags,
image_hash: entity.image_hash,
created_at: entity.created_at && entity.created_at.getTime() / 1000,
updated_at: entity.updated_at && entity.updated_at.getTime() / 1000,
factorioprints_id: entity.factorioprints_id ?? null,
@ -78,30 +79,44 @@ export async function searchBlueprintPages({
export async function createBlueprintPage(
type: "blueprint" | "blueprint_book",
targetId: string,
extraInfo: {
data: {
title: string;
user_id: string | null;
description_markdown: string;
tags?: string[];
image_hash: string;
created_at?: number;
updated_at?: number;
firstBlueprintId?: string;
factorioprints_id?: string;
}
) {
const page = await prisma.blueprint_page.create({
data: {
user_id: extraInfo.user_id,
title: extraInfo.title,
description_markdown: extraInfo.description_markdown,
factorioprints_id: extraInfo.factorioprints_id,
user_id: data.user_id,
title: data.title,
description_markdown: data.description_markdown,
factorioprints_id: data.factorioprints_id,
blueprint_id: type === "blueprint" ? targetId : undefined,
blueprint_book_id: type === "blueprint_book" ? targetId : undefined,
tags: extraInfo.tags ? extraInfo.tags : [],
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(),
tags: data.tags ? data.tags : [],
image_hash: data.image_hash,
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`);
return page;
}

View File

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

View File

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

View File

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

View File

@ -38,4 +38,7 @@ const promise = _init()
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 pako from "pako";
import * as cookie from "cookie";
import { BlueprintStringData } from "@factorio-sites/common-utils";
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 (
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;
description_markdown: string;
tags: string[];
image_hash: string;
created_at: number;
updated_at: number;
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",
"affected": {
"defaultBase": "master"
},
"affected": { "defaultBase": "master" },
"implicitDependencies": {
"workspace.json": "*",
"package.json": {
"dependencies": "*",
"devDependencies": "*"
},
"package.json": { "dependencies": "*", "devDependencies": "*" },
"tsconfig.base.json": "*",
"tslint.json": "*",
".eslintrc.json": "*",
@ -17,36 +12,18 @@
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/workspace/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"]
}
"options": { "cacheableOperations": ["build", "lint", "test", "e2e"] }
}
},
"projects": {
"blueprints": {
"tags": []
},
"blueprints-e2e": {
"tags": [],
"implicitDependencies": ["blueprints"]
},
"blueprint-image-function": {
"tags": []
},
"factorioprints-scraper": {
"tags": []
},
"database": {
"tags": []
},
"node-utils": {
"tags": []
},
"common-utils": {
"tags": []
},
"web-utils": {
"tags": []
}
"blueprints": { "tags": [] },
"blueprints-e2e": { "tags": [], "implicitDependencies": ["blueprints"] },
"blueprint-image-function": { "tags": [] },
"factorioprints-scraper": { "tags": [] },
"database": { "tags": [] },
"node-utils": { "tags": [] },
"common-utils": { "tags": [] },
"web-utils": { "tags": [] },
"types": { "tags": [] }
}
}

View File

@ -20,7 +20,8 @@
"@factorio-sites/database": ["libs/database/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/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"]

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": {

View File

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