@knapsack/app
Version:
Build Design Systems on top of knapsack, by Basalt
563 lines (524 loc) • 15.4 kB
text/typescript
/**
* Copyright (C) 2018 Basalt
This file is part of Knapsack.
Knapsack is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
Knapsack is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along
with Knapsack; if not, see <https://www.gnu.org/licenses>.
*/
import express from 'express';
import urlJoin from 'url-join';
import md from 'marked';
import fs from 'fs-extra';
import { join, relative, parse as parsePath } from 'path';
import { exec } from 'child_process';
import highlight from 'highlight.js';
import { paramCase } from 'change-case';
import shortid from 'shortid';
import multer from 'multer';
import * as Files from '../schemas/api/files';
import { endpoint as pluginsEndpoint } from '../schemas/api/plugins';
import { handlePluginsEndpoint } from './api/plugins';
import { MemDb } from './dbs/mem-db';
import * as log from '../cli/log';
import { BASE_PATHS, HTTP_STATUS } from '../lib/constants';
import { getUserInfo } from './auth';
import { KnapsackBrain, KnapsackConfig } from '../schemas/main-types';
import { KnapsackMeta, FileResponse } from '../schemas/misc';
import { KnapsackTemplateDemo } from '../schemas/patterns';
import { KsRenderResults } from '../schemas/knapsack-config';
import { fileExists, resolvePath } from './server-utils';
import { getFeaturesForUser } from '../lib/features';
import { PatternRenderData } from '../schemas/api/render';
const router = express.Router();
const memDb = new MemDb<KnapsackTemplateDemo>();
// https://marked.js.org/#/USING_ADVANCED.md
md.setOptions({
highlight: code => highlight.highlightAuto(code).value,
});
export function getApiRoutes({
registerEndpoint,
patternManifest,
pageBuilder,
settingsStore,
meta,
baseUrl,
tokens,
config,
}: {
patternManifest: KnapsackBrain['patterns'];
config: KnapsackBrain['config'];
webroot: string;
public: string;
baseUrl: string;
meta: KnapsackMeta;
tokens: KnapsackBrain['tokens'];
pageBuilder: KnapsackBrain['pageBuilderPages'];
settingsStore: KnapsackBrain['settings'];
registerEndpoint: (pathname: string, method?: 'GET' | 'POST') => void;
templateRenderers: KnapsackConfig['templateRenderers'];
}): typeof router {
router.get(baseUrl, (req, res) => {
res.json({
ok: true,
message: 'Welcome to the API!',
});
});
router.post(pluginsEndpoint, handlePluginsEndpoint);
{
const url = urlJoin(baseUrl, 'data');
registerEndpoint(url, 'POST');
router.post(url, async (req, res) => {
const { body, headers } = req;
if (headers['content-type'] !== 'application/json') {
res.send({
ok: false,
message:
"Must send with a Header of 'Content-Type: application/json'",
});
return;
}
const hash = memDb.addData(body);
res.send({
ok: true,
message: `Data has been made and can be viewed with hash "${hash}"`,
data: {
hash,
},
});
});
}
if (patternManifest) {
async function render({
patternId,
templateId,
demoId,
assetSetId,
isInIframe,
dataId,
}: {
patternId: string;
templateId: string;
assetSetId?: string;
/**
* Data id from `saveData()`
* Cannot use with `demoId`
*/
dataId?: string;
/**
* Cannot use with `dataId`
*/
demoId?: string;
/**
* Will this be in an iFrame?
*/
isInIframe?: boolean;
}): Promise<KsRenderResults> {
if (demoId && dataId) {
const message = `Cannot send query params "demoId" and "dataId" to render endpont for pattern "${patternId}", template "${templateId}"`;
log.error(message, {
patternId,
templateId,
demoId,
assetSetId,
isInIframe,
dataId,
});
return {
ok: false,
html: `<p>${message}</p>`,
wrappedHtml: `<p>${message}</p>`,
message,
dataId: '',
};
}
const demo = dataId ? memDb.getData(dataId) : demoId;
// log.inspect({ demoId, demo });
try {
const results = patternManifest.render({
patternId,
templateId,
demo,
isInIframe,
websocketsPort: meta.websocketsPort,
assetSetId,
// demoDataId,
});
return results;
} catch (e) {
return {
ok: false,
html: `<p>${e.message}</p>`,
wrappedHtml: `<p>${e.message}</p>`,
message: e.message,
dataId: '',
};
}
}
const url = urlJoin(baseUrl, '/render');
registerEndpoint(url, 'GET');
registerEndpoint(url, 'POST');
router.post(url, async (req, res) => {
const { body } = req;
const {
patternId,
templateId,
isInIframe,
assetSetId,
demoId,
dataId,
}: PatternRenderData = body;
if (!(patternId && templateId)) {
res.send({
ok: false,
message: `Missing patternId or templateId in body`,
data: body,
});
} else {
const results = await render({
patternId,
templateId,
isInIframe,
assetSetId,
demoId,
dataId,
});
res.send(results);
}
});
router.get(url, async (req, res) => {
const { query } = req;
query.wrapHtml = query.wrapHtml === 'true';
query.isInIframe = query.isInIframe === 'true';
const {
patternId,
templateId,
isInIframe,
wrapHtml,
assetSetId,
demoId,
dataId,
}: PatternRenderData = query;
const results = await render({
patternId,
templateId,
assetSetId,
dataId,
demoId,
isInIframe,
});
if (results.ok) {
res.send(wrapHtml ? results.wrappedHtml : results.html);
} else {
let demo;
if (dataId) {
demo = memDb.getData(dataId);
}
log.error(`Error rendering template`, {
patternId,
templateId,
dataId,
wrapHtml,
isInIframe,
assetSetId,
message: results.message,
demo,
});
console.log(); // empty line
res.send(results.message);
}
});
}
if (patternManifest) {
const url1 = urlJoin(baseUrl, 'pattern/:id');
registerEndpoint(url1);
router.get(url1, async (req, res) => {
const results = await patternManifest.getPattern(req.params.id);
res.send(results);
});
const url2 = urlJoin(baseUrl, 'patterns');
registerEndpoint(url2);
router.get(url2, async (req, res) => {
const results = await patternManifest.getPatterns();
res.send(results);
});
{
const url = Files.endpoint;
registerEndpoint(url);
router.post(url, async (req, res) => {
const userInfo = getUserInfo(req);
const { isLocalDev } = getFeaturesForUser(userInfo);
const localOnly = () =>
res.status(HTTP_STATUS.BAD.BAD_REQUEST).send({
ok: false,
message: 'This endpoint only available to local developers',
});
let response: Files.ActionResponses;
const reqBody: Files.Actions = req.body;
const { data: dataDir } = config;
switch (reqBody.type) {
case Files.ACTIONS.verify: {
if (!isLocalDev) {
return localOnly();
}
const { path } = reqBody.payload;
const { exists, absolutePath, type } = resolvePath({
path,
resolveFromDirs: [dataDir],
});
response = {
type: Files.ACTIONS.verify,
payload: {
exists,
relativePath: exists ? relative(dataDir, absolutePath) : '',
absolutePath,
type,
},
};
break;
}
case Files.ACTIONS.saveTemplateDemo: {
if (!isLocalDev) {
return localOnly();
}
const { patternId, templateId, demoId, code } = reqBody.payload;
const fullPath = patternManifest.getTemplateDemoAbsolutePath({
patternId,
templateId,
demoId,
});
try {
await fs.writeFile(fullPath, code, 'utf8');
response = {
type: Files.ACTIONS.saveTemplateDemo,
payload: {
ok: true,
},
};
} catch (e) {
response = {
type: Files.ACTIONS.saveTemplateDemo,
payload: {
ok: false,
message: e.message,
},
};
}
break;
}
case Files.ACTIONS.deleteTemplateDemo: {
if (!isLocalDev) {
return localOnly();
}
const { path } = reqBody.payload;
const { exists, absolutePath } = resolvePath({
path,
resolveFromDirs: [dataDir],
});
if (!exists) {
response = {
type: Files.ACTIONS.deleteTemplateDemo,
payload: {
ok: false,
message: 'File already did not exist',
},
};
} else {
try {
await fs.remove(absolutePath);
response = {
type: Files.ACTIONS.deleteTemplateDemo,
payload: {
ok: true,
},
};
} catch (e) {
log.error('Files.ACTIONS.deleteTemplateDemo', e, '/api/files');
response = {
type: Files.ACTIONS.deleteTemplateDemo,
payload: {
ok: false,
message: e.message,
},
};
}
}
break;
}
case Files.ACTIONS.openFile: {
if (!isLocalDev) {
return localOnly();
}
const { filePath } = reqBody.payload;
const { exists, absolutePath } = resolvePath({
path: filePath,
resolveFromDirs: [dataDir],
});
if (exists) {
exec(`open ${absolutePath}`, err => {
if (err)
log.error(`error opening file: ${err.message}`, {
filePath,
err,
});
response = {
type: Files.ACTIONS.openFile,
payload: {
ok: !err,
message: err ? err.message : '',
},
};
});
}
}
}
res.send(response);
});
}
}
if (pageBuilder) {
const url1 = urlJoin(baseUrl, `${BASE_PATHS.PAGE_BUILDER}/:id`);
registerEndpoint(url1);
router.get(url1, async (req, res) => {
try {
const page = await pageBuilder.getPageBuilderPage(req.params.id);
res.send({
ok: true,
page,
});
} catch (error) {
if (error.code === 'ENOENT') {
res.send({
ok: false,
message: `Example "${req.params.id}" not found.`,
});
} else {
res.send({
ok: false,
message: error.toString(),
});
}
}
});
const url2 = urlJoin(baseUrl, `${BASE_PATHS.PAGE_BUILDER}/:id`);
registerEndpoint(url2, 'POST');
router.post(url2, async (req, res) => {
const results = await pageBuilder.setPageBuilderPage(
req.params.id,
req.body,
);
res.send(results);
});
const url3 = urlJoin(baseUrl, BASE_PATHS.PAGE_BUILDER);
registerEndpoint(url3);
router.get(url3, async (req, res) => {
const results = await pageBuilder.getPageBuilderPages();
res.send(results);
});
} else {
router.get(urlJoin(baseUrl, BASE_PATHS.PAGE_BUILDER), async (req, res) => {
res.send([]);
});
}
const url3 = urlJoin(baseUrl, 'settings');
registerEndpoint(url3);
router.get(url3, async (req, res) => {
const settings = settingsStore.getData();
res.send(settings);
});
const url4 = urlJoin(baseUrl, 'design-tokens');
registerEndpoint(url4);
router.get(url4, (req, res) => {
res.send(tokens.getTokens());
});
const url5 = urlJoin(baseUrl, 'meta');
registerEndpoint(url5);
router.get(url5, (req, res) => {
res.send(meta);
});
{
const url = urlJoin(baseUrl, 'loading');
registerEndpoint(url);
router.get(url, (req, res) => {
res.send(`<p>Loading...</p>`);
});
}
const url6 = urlJoin(baseUrl, 'permissions');
registerEndpoint(url6);
router.get(url6, (req, res) => {
const { role } = getUserInfo(req);
res.send(role.permissions);
});
const destination = join(config.public, 'uploads');
const uploaderLocal = multer({
dest: config.public,
storage: multer.diskStorage({
destination,
filename(req, file, cb) {
const { originalname } = file;
const { name, ext } = parsePath(originalname);
let filename = `${paramCase(name)}${ext}`;
if (fs.existsSync(join(destination, filename))) {
filename = `${paramCase(name)}-${shortid.generate()}${ext}`;
}
cb(null, filename);
},
}),
});
const uploadLocalSingle = uploaderLocal.single('file');
const url7 = urlJoin(baseUrl, 'upload');
registerEndpoint(url7);
router.post(url7, (req, res) => {
let resData: FileResponse;
uploadLocalSingle(req, res, err => {
const { file } = req as Express.Request;
if (err instanceof multer.MulterError) {
resData = {
ok: false,
message: err.message,
};
return res.send(resData);
}
if (err) {
resData = {
ok: false,
message: err.message,
};
return res.send(resData);
}
if (!file) {
resData = {
ok: false,
message: 'Did not receive a file',
};
return res.send(resData);
}
const {
path,
size,
mimetype,
originalname,
filename,
} = file as Express.Multer.File;
const publicPath = `/${relative(config.public, path)}`;
resData = {
ok: true,
data: {
publicPath,
size,
mimetype,
originalName: originalname,
filename,
},
};
return res.status(200).send(resData);
});
});
return router;
}