moleculer-api
Version:
A dynamic API Gateway for MoleculerJS which updates REST endpoints and aggregated GraphQL schema, access control policy for each action calls from metadata of remote services schema without restart or deployment.
396 lines • 16.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs_1 = tslib_1.__importDefault(require("fs"));
const path_1 = tslib_1.__importDefault(require("path"));
const memorystream_1 = tslib_1.__importDefault(require("memorystream"));
const __1 = require("../../");
const test_1 = require("../../test");
/* create gateway and run */
const gateway = new __1.APIGateway({
brokers: [
{
moleculer: {
nodeID: "gateway-node",
namespace: "dev-gateway",
transporter: {
type: "TCP",
udpPeriod: 1,
},
},
},
],
schema: {
branch: {
maxVersions: 1,
},
},
server: {
update: {
debouncedSeconds: 0,
},
protocol: {
http: {
port: 8080,
},
},
context: {
auth: {
parser: __1.createAuthContextOIDCParser({
issuer: "https://account.dev.qmit.pro",
client_id: "test",
client_secret: "3322b0c4c46443c88770041d05531dc994c8121d36ee4a21928c8626b09739d7",
}),
},
},
middleware: {
cors: false,
serveStatic: {
dirRootPath: __dirname + "/__test__",
routeBasePath: "/",
},
},
},
logger: {
winston: { level: "info" },
},
});
const services = test_1.getMoleculerServiceBroker({
moleculer: {
nodeID: "remote-node",
namespace: "dev-gateway",
transporter: {
type: "TCP",
udpPeriod: 1,
},
},
services: [
{
name: "chat",
metadata: {
api: {
branch: "master",
policy: {},
protocol: {
WebSocket: {
basePath: "/chat",
description: "...",
routes: [
/* bidirectional streaming chat */
{
path: "/message-stream/:roomId",
call: {
action: "chat.message.stream",
params: {
roomId: "@path.roomId",
},
},
},
/* pub/sub chat */
{
path: "/message-pubsub/:roomId",
subscribe: {
events: `({ path }) => ["chat.message." + path.roomId]`,
},
publish: {
event: `({ path }) => "chat.message." + path.roomId`,
params: "@message",
},
},
/* pub/sub video */
{
path: "/video-pubsub",
subscribe: {
events: ["chat.video"],
},
publish: {
event: "chat.video",
params: {
id: "@context.id",
username: "@query.username",
data: "@message",
},
filter: `({ params }) => params.id && params.username && params.data`,
},
},
/* streaming video */
{
path: "/video-stream/:type",
call: {
action: "chat.video.stream",
params: {
id: "@context.id",
type: "@path.type",
},
},
},
],
},
},
},
},
actions: {
"message.stream": {
handler(ctx) {
// bidirectional
const serverStream = new memorystream_1.default("---initial-content-from-server---");
const clientStream = ctx.params;
clientStream.on("data", data => {
serverStream.write("---remote service received data and echo: " + data.toString() + "---");
});
return serverStream;
},
},
"video.stream": {
handler(ctx) {
// mono-directional
const { id, type } = (ctx.meta || {});
if (!id || !type) {
throw new Error("invalid params");
}
if (type === "server") {
// store server stream
if (this.serverStream) {
throw new Error("server is already going on");
}
const stream = ctx.params;
if (!stream || !stream.pipe) {
throw new Error("invalid stream for server");
}
this.serverStream = stream;
if (!this.clientStreams) {
this.clientStreams = [];
}
stream.on("close", () => {
delete this.serverStream;
delete this.metaChunks;
});
this.metaChunks = null;
stream.on("data", data => {
this.lastPacketTime = new Date().getTime();
if (!this.metaChunks) {
this.metaChunks = [];
}
if (this.metaChunks.length < 2) {
this.metaChunks.push(data);
console.log(`collecting meta chunks... (${this.metaChunks.length}/2)`);
}
this.clientStreams.forEach((clientStream) => clientStream.write(data));
});
this.interval = setInterval(() => {
if (this.lastPacketTime && new Date().getTime() - this.lastPacketTime > 1000 * 5) {
// @ts-ignore
stream.destroy();
clearInterval(this.interval);
delete this.interval;
console.log("server stream closed due to interval timeout 5s", id);
}
}, 1000);
console.log("server stream created", id);
}
else {
if (!this.serverStream) {
throw new Error("there are no server going on");
}
// new stream for client
const stream = new memorystream_1.default();
console.log("client stream created", id);
if (this.metaChunks) {
console.log(`send meta chunks (${this.metaChunks.length}) to ${id}`);
for (const chunk of this.metaChunks) {
stream.write(chunk);
}
}
stream.on("close", () => {
const index = this.clientStreams.indexOf(stream);
if (index !== -1) {
this.clientStreams.splice(index, 1);
console.log("client stream closed", id);
}
});
this.clientStreams.push(stream);
return stream;
}
},
},
},
},
{
name: "file",
metadata: {
api: {
branch: "master",
protocol: {
REST: {
description: "..",
basePath: "/file",
routes: [
{
method: "POST",
path: "/",
call: {
action: "file.upload",
params: "@body.file",
},
},
{
method: "GET",
path: "/:filename",
call: {
action: "file.get",
params: {
filename: "@path.filename",
},
},
},
],
},
},
policy: {},
},
},
actions: {
upload(ctx) {
return new Promise((resolve, reject) => {
if (!ctx.params || !ctx.params.pipe) {
throw new Error("no file to upload");
}
const meta = ctx.meta;
const saveStream = fs_1.default.createWriteStream(path_1.default.join(__dirname, "..", "tmp." + (meta.filename || "unknown-file")));
ctx.params.pipe(saveStream);
ctx.params.on("end", () => resolve(meta));
ctx.params.on("error", reject);
});
},
get: {
params: {
filename: "string",
},
handler(ctx) {
const filepath = path_1.default.join(__dirname, "..", "tmp." + ctx.params.filename);
if (!fs_1.default.statSync(filepath).isFile()) {
throw new Error("no such file");
}
// ctx.meta.$headers = {
// Location: "https://google.com",
// };
// ctx.meta.$status = 302;
return fs_1.default.createReadStream(filepath);
},
},
},
},
{
name: "foo",
metadata: {
api: {
branch: "master",
protocol: {
REST: {
description: "test..",
basePath: "/foo",
routes: [
{
method: "GET",
path: "/bar",
map: `() => { throw new Error("what an error"); }`,
},
{
method: "GET",
path: "/:id",
call: {
action: "foo.get",
params: {},
},
},
{
method: "GET",
path: "/:a/:b/:c?",
map: `(args) => args`,
},
],
},
GraphQL: {
description: "blablabla",
typeDefs: `
interface Node {
id: String
}
type Foo implements Node {
id: String
}
extend type Query {
foo(id: ID!): Foo
}
extend type Mutation {
uploadFile(file: Upload!): File!
}
type File {
mimetype: String
encoding: String
filename: String
}
`,
resolvers: {
Foo: {
id: `({ source }) => source.id`,
__isTypeOf: `({ source }) => source && source.__isFoo`,
},
Query: {
foo: {
call: {
action: "foo.get",
params: {
id: "@args.id[]",
},
map: `({ response }) => ({ ...response, __isFoo: true })`,
},
ignoreError: true,
},
},
Mutation: {
uploadFile: {
call: {
action: "foo.noop",
params: {
file: "@args.file",
},
map: `({ request }) => request.params.file`,
},
},
},
},
},
},
policy: {},
},
},
actions: {
get: {
params: {
id: ["string", {
type: "array",
items: "string",
}],
},
handler(ctx) {
const id = ctx.params.id;
if (Array.isArray(id)) { // batching
// tslint:disable-next-line:no-shadowed-variable
return id.map(id => ({ id }));
}
return { id }; // single
},
},
noop(ctx) {
console.log("foo.noop got:", ctx.params);
},
},
},
],
});
(() => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
yield gateway.start();
yield services.start();
}))();
//# sourceMappingURL=index.js.map