metamigo-api: runs in docker now

* great typescript module import refactor
* refactor metamigo-cli so it is the entrypoint for the db, api, and
  worker
This commit is contained in:
Abel Luck 2023-06-02 14:05:20 +00:00
parent b45e2e8c11
commit 696ba16cb7
78 changed files with 319 additions and 180 deletions

View file

@ -0,0 +1,12 @@
require("eslint-config-link/patch/modern-module-resolution");
module.exports = {
extends: [
"eslint-config-link/profile/node",
"eslint-config-link/profile/typescript",
"eslint-config-link/profile/jest",
],
parserOptions: { tsconfigRootDir: __dirname },
rules: {
"new-cap": "off"
},
};

View file

@ -0,0 +1,54 @@
FROM node:20 as base
FROM base AS builder
ARG APP_DIR=/opt/metamigo-cli
RUN mkdir -p ${APP_DIR}/
RUN npm i -g turbo
WORKDIR ${APP_DIR}
COPY . .
RUN turbo prune --scope=@digiresilience/metamigo-cli --docker
FROM base AS installer
ARG APP_DIR=/opt/metamigo-cli
WORKDIR ${APP_DIR}
COPY .gitignore .gitignore
COPY --from=builder ${APP_DIR}/out/json/ .
COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
RUN npm ci --omit=dev
COPY --from=builder ${APP_DIR}/out/full/ .
RUN npm i -g turbo
RUN turbo run build --filter=metamigo-cli
FROM base AS runner
ARG APP_DIR=/opt/metamigo-cli
WORKDIR ${APP_DIR}/
ARG BUILD_DATE
ARG VERSION
LABEL maintainer="Darren Clarke <darren@redaranj.com>"
LABEL org.label-schema.build-date=$BUILD_DATE
LABEL org.label-schema.version=$VERSION
ENV APP_DIR ${APP_DIR}
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y --no-install-recommends \
dumb-init
RUN mkdir -p ${APP_DIR}
RUN chown -R node ${APP_DIR}/
USER node
WORKDIR ${APP_DIR}
COPY --from=installer ${APP_DIR}/node_modules/ ./node_modules/
COPY --from=installer ${APP_DIR}/packages/ ./packages/
COPY --from=installer ${APP_DIR}/apps/metamigo-cli/ ./apps/metamigo-cli/
COPY --from=installer ${APP_DIR}/apps/metamigo-api/ ./apps/metamigo-api/
COPY --from=installer ${APP_DIR}/apps/metamigo-worker/ ./apps/metamigo-worker/
COPY --from=installer ${APP_DIR}/package.json ./package.json
USER root
WORKDIR ${APP_DIR}/apps/metamigo-cli/
RUN chmod +x docker-entrypoint.sh
USER node
EXPOSE 3000
ENV PORT 3000
ENV NODE_ENV production
ENTRYPOINT ["/opt/metamigo-cli/apps/metamigo-cli/docker-entrypoint.sh"]

View file

@ -0,0 +1,3 @@
{
"presets": ["babel-preset-link"]
}

4
apps/metamigo-cli/cli Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
node ./build/main/index.js ${@}

View file

@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e
if [[ "$1" == "api" ]]; then
echo "docker-entrypoint: starting api server"
./cli db -- migrate
exec dumb-init ./cli api
elif [[ "$1" == "worker" ]]; then
echo "docker-entrypoint: starting worker"
exec dumb-init ./cli worker
elif [[ "$1" == "frontend" ]]; then
echo "docker-entrypoint: starting frontend"
exec dumb-init yarn workspace @app/frontend start
elif [[ "$1" == "cli" ]]; then
echo "docker-entrypoint: starting frontend"
shift 1
exec ./cli "$@"
else
echo "docker-entrypoint: missing argument, one of: api, worker, frontend, cli"
exit 1
fi

View file

@ -0,0 +1,4 @@
{
"preset": "jest-config-link",
"setupFiles": ["<rootDir>/src/setup.test.ts"]
}

View file

@ -0,0 +1,42 @@
{
"name": "@digiresilience/metamigo-cli",
"version": "0.2.0",
"main": "build/main/index.js",
"author": "Abel Luck <abel@guardianproject.info>",
"license": "AGPL-3.0-or-later",
"type": "module",
"bin": {
"metamigo": "./build/main/index.js"
},
"dependencies": {
"@digiresilience/montar": "*",
"@digiresilience/metamigo-config": "*",
"@digiresilience/metamigo-db": "*",
"@digiresilience/metamigo-api": "*",
"@digiresilience/metamigo-worker": "*",
"commander": "^10.0.1",
"graphile-migrate": "^1.4.1",
"graphile-worker": "^0.13.0",
"node-jose": "^2.2.0",
"postgraphile": "4.13.0",
"graphql": "16.6.0"
},
"devDependencies": {
"@types/jest": "^29.5.1",
"pino-pretty": "^10.0.0",
"nodemon": "^2.0.22",
"tsconfig-link": "*",
"eslint-config-link": "*",
"jest-config-link": "*",
"babel-preset-link": "*",
"typescript": "^5.0.4"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"cli": "NODE_ENV=development node --unhandled-rejections=strict build/main/index.js",
"fix:lint": "eslint src --ext .ts --fix",
"fmt": "prettier \"src/**/*.ts\" --write",
"lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different",
"test": "echo no tests"
}
}

View file

@ -0,0 +1,22 @@
import {
generateConfig,
printConfigOptions,
} from "@digiresilience/metamigo-common";
import { IAppConfig, IAppConvict } from "@digiresilience/metamigo-config";
import { loadConfigRaw } from "@digiresilience/metamigo-config";
export const genConf = async (): Promise<void> => {
const c = await loadConfigRaw() as any;
const generated = generateConfig(c) as any;
console.log(generated);
};
export const genSchema = async (): Promise<void> => {
const c: any = await loadConfigRaw();
console.log(c.getSchemaString());
};
export const listConfig = async (): Promise<void> => {
const c = await loadConfigRaw() as any;
printConfigOptions(c);
};

View file

@ -0,0 +1,66 @@
#!/usr/bin/env node
import { Command } from "commander";
import { startWithout } from "@digiresilience/montar";
import { migrateWrapper } from "@digiresilience/metamigo-db";
import { loadConfig } from "@digiresilience/metamigo-config";
import { genConf, listConfig } from "./config.js";
import { createTokenForTesting, generateJwks } from "./jwks.js";
import { exportGraphqlSchema } from "./metamigo-postgraphile.js";
import "@digiresilience/metamigo-api";
import "@digiresilience/metamigo-worker";
const program = new Command();
export async function runServer(): Promise<void> {
await startWithout(["worker"]);
}
export async function runWorker(): Promise<void> {
await startWithout(["server"]);
}
program
.command("config-generate")
.description("Generate a sample JSON configuration file (to stdout)")
.action(genConf);
program
.command("config-help")
.description("Prints the entire convict config ")
.action(listConfig);
program
.command("api")
.description("Run the application api server")
.action(runServer);
program
.command("worker")
.description("Run the worker to process jobs")
.action(runWorker);
program
.command("db <commands...>")
.description("Run graphile-migrate commands with your app's config loaded.")
.action(async (args) => {
const config = await loadConfig();
return migrateWrapper(args, config);
});
program
.command("gen-jwks")
.description("Generate the JWKS")
.action(generateJwks);
program
.command("gen-testing-jwt")
.description("Generate a JWT for the test suite")
.action(createTokenForTesting);
program
.command("export-graphql-schema")
.description("Export the graphql schema")
.action(exportGraphqlSchema);
program.parse(process.argv);

View file

@ -0,0 +1,67 @@
import jose from "node-jose";
import * as jwt from "jsonwebtoken";
const generateKeystore = async () => {
const keystore = jose.JWK.createKeyStore();
await keystore.generate("oct", 256, {
alg: "A256GCM",
use: "enc",
});
await keystore.generate("oct", 256, {
alg: "HS512",
use: "sig",
});
return keystore;
};
const safeString = (input) =>
Buffer.from(JSON.stringify(input)).toString("base64");
const stringify = (v) => JSON.stringify(v, undefined, 2);
const _generateJwks = async () => {
const keystore = await generateKeystore();
const encryption = keystore.all({ use: "enc" })[0].toJSON(true);
const signing = keystore.all({ use: "sig" })[0].toJSON(true);
return {
nextAuth: {
signingKeyB64: safeString(signing),
encryptionKeyB64: safeString(encryption),
},
};
};
export const generateJwks = async (): Promise<void> => {
console.log(stringify(await _generateJwks()));
};
export const createTokenForTesting = async (): Promise<void> => {
const keys = await _generateJwks();
const signingKey = Buffer.from(
JSON.parse(
Buffer.from(keys.nextAuth.signingKeyB64, "base64").toString("utf-8")
).k,
"base64"
);
const token = jwt.sign(
{
iss: "Test Env",
iat: 1606893960,
aud: "metamigo",
sub: "abel@guardianproject.info",
name: "Abel Luck",
email: "abel@guardianproject.info",
userRole: "admin",
},
signingKey,
{ expiresIn: "100y", algorithm: "HS512" }
);
console.log("CONFIG");
console.log(stringify(keys));
console.log();
console.log("TOKEN");
console.log(token);
console.log();
};

View file

@ -0,0 +1,40 @@
import { writeFileSync } from "node:fs";
import {
getIntrospectionQuery,
GraphQLSchema,
graphqlSync,
lexicographicSortSchema,
printSchema,
} from "graphql";
import { createPostGraphileSchema } from "postgraphile";
import pg from "pg";
import { loadConfig } from "@digiresilience/metamigo-config";
import { getPostGraphileOptions } from "@digiresilience/metamigo-db";
const {Pool} = pg;
export const exportGraphqlSchema = async (): Promise<void> => {
const config = await loadConfig();
const rootPgPool = new Pool({
connectionString: config.db.connection,
});
const exportSchema = `../../data/schema.graphql`;
const exportJson = `../../frontend/lib/graphql-schema.json`;
try {
const schema = (await createPostGraphileSchema(
config.postgraphile.authConnection,
"app_public",
getPostGraphileOptions()
)) as unknown as GraphQLSchema;
const sorted = lexicographicSortSchema(schema);
const json = graphqlSync({ schema, source: getIntrospectionQuery() });
writeFileSync(exportSchema, printSchema(sorted));
writeFileSync(exportJson, JSON.stringify(json));
console.log(`GraphQL schema exported to ${exportSchema}`);
console.log(`GraphQL schema json exported to ${exportJson}`);
} finally {
rootPgPool.end();
}
};

View file

@ -0,0 +1,14 @@
{
"extends": "tsconfig-link",
"compilerOptions": {
"incremental": true,
"outDir": "build/main",
"rootDir": "src",
"baseUrl": "./",
"skipLibCheck": true,
"types": ["jest", "node"],
"esModuleInterop": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**"]
}