All pages
Powered by GitBook
1 of 1

Loading...

moleculer-api

A dynamic API Gateway which updates REST endpoints, GraphQL schema, WebSocket handlers and access control policies by integrating metadata of discovered remote services.

moleculer-api๋Š” MSA ํ™˜๊ฒฝ์—์„œ ๋งˆ์ดํฌ๋กœ ์„œ๋น„์Šค๋“ค์˜ API ์Šคํ‚ค๋งˆ ์กฐ๊ฐ์„ ์ˆ˜์ง‘ํ•˜๊ณ , ๋ฌด์ค‘๋‹จ์œผ๋กœ ํ†ตํ•ฉ API๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ œ๊ณตํ•˜๋Š” ์›น ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

์„œ๋น„์Šค API ์Šคํ‚ค๋งˆ๋Š” ๋ถ„์‚ฐ ์„œ๋น„์Šค ํ”„๋กœ์‹œ์ €์˜ ํ˜ธ์ถœ, ๋˜๋Š” ์ค‘์•™ ๋ฉ”์‹œ์ง• ์„œ๋น„์Šค์— ๋Œ€ํ•œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐ ๊ตฌ๋…์„ ์‘์šฉ ํ”„๋กœํ† ์ฝœ(REST, GraphQL, WebSocket ๋“ฑ)์— ๋งตํ•‘ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋น„์Šค API ์Šคํ‚ค๋งˆ๋Š” JSON ํฌ๋งท์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด์žˆ์œผ๋ฉฐ, ์‘์šฉ ํ”„๋กœํ† ์ฝœ ๋ณ„ API ๋งตํ•‘๊ณผ ๊ทธ์— ๋Œ€ํ•œ ์ ‘๊ทผ ์ œ์–ด๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์„œ๋น„์Šค API ์Šคํ‚ค๋งˆ ์˜ˆ์‹œ

Gateway๋Š” ํŠน์ • ์„œ๋น„์Šค API ์Šคํ‚ค๋งˆ์˜ ์ถ”๊ฐ€, ์ œ๊ฑฐ ๋ฐ ์—…๋ฐ์ดํŠธ์‹œ ๊ธฐ์กด ํ†ตํ•ฉ API ์Šคํ‚ค๋งˆ์— ๋ณ‘ํ•ฉ์„ ์‹œ๋„ํ•˜๊ณ , ์„ฑ๊ณต์‹œ ๋ฌด์ค‘๋‹จ์œผ๋กœ ๋ผ์šฐํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ฉฐ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์›๊ฒฉ ์„œ๋น„์Šค์— ๋‹ค์‹œ ๋ณด๊ณ ํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ๊ธฐ๋Šฅ

  • ๋ถ„์‚ฐ ์„œ๋น„์Šค์˜ API ์Šคํ‚ค๋งˆ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ๋ณ‘ํ•ฉํ•˜์—ฌ API๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ

  • Polyglot ํ•˜๊ฑฐ๋‚˜ ์„œ๋กœ ๋‹ค๋ฅธ ์„œ๋น„์Šค ๋ธŒ๋กœ์ปค์— ๊ธฐ๋ฐ˜ํ•œ ์„œ๋น„์Šค๋“ค์˜ API ์Šคํ‚ค๋งˆ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ๋ณ‘ํ•ฉ ํ•  ์ˆ˜ ์žˆ์Œ

  • ๊ฐœ๋ฐœ ํŽธ์˜ ๋ฐ ์ถฉ๋Œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ API ๋ธŒ๋žœ์นญ ๋ฐ ๋ฒ„์ €๋‹ ๊ธฐ๋Šฅ

  • ์ƒํƒœ ๊ฒ€์‚ฌ ๋ฐ API ๋ฌธ์„œ ์ƒ์„ฑ (WIP)

License

The project is available under the .

๋ฏธ๋“ค์›จ์–ด ๋ฐฉ์‹์˜ ์š”์ฒญ ํ๋ฆ„ ์ œ์–ด

  • Error

  • Logging

  • Body Parser

  • Helmet

  • CORS

  • Serve Static File

  • HTTP/HTTPS/HTTP2

  • (ํ™•์žฅ ๊ฐ€๋Šฅ)

  • ๋ฏธ๋“ค์›จ์–ด ๋ฐฉ์‹์˜ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ ์ œ์–ด

    • Authn/Authz

    • Locale

    • Correlation ID

    • IP Address

    • User-Agent

    • Request

    • (ํ™•์žฅ ๊ฐ€๋Šฅ)

  • ์‘์šฉ ํ”„๋กœํ† ์ฝœ ํ”Œ๋Ÿฌ๊ทธ์ธ

    • REST

    • GraphQL

    • WebSocket

    • (ํ™•์žฅ ๊ฐ€๋Šฅ)

  • ์ ‘๊ทผ ์ œ์–ด ์ •์ฑ… ํ”Œ๋Ÿฌ๊ทธ์ธ

    • OAuth2 scope ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด

    • JavaScript FBAC; Function Based Access Control ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด

    • (ํ™•์žฅ ๊ฐ€๋Šฅ)

  • MIT license
    Gateway์˜ ๋ฌด์ค‘๋‹จ ์—”๋“œํฌ์ธํŠธ ์—…๋ฐ์ดํŠธ ๋ฐ ๋ณด๊ณ 
    {
      branch: "master",
      policy: {},
      protocol: {
        REST: {
          basePath: "/storage",
          routes: [
            {
              path: "/",
              method: "GET",
              call: {
                action: "storage.get",
                params: {
                  offset: "@query.offset:number",
                  limit: "@query.limit:number",
                },
              },
            },
            {
              path: "/upload",
              method: "POST",
              call: {
                action: "storage.create",
                params: {
                  file: "@body.file",
                  meta: {
                    tags: {
                      identityId: "@context.auth.identity.sub",
                    },
                    allowedContentTypes: ["text/*", "image/*", "application/pdf"],
                    private: false,
                  },
                },
              },
            },
            {
              path: "/upload-stream",
              method: "POST",
              description: "not a production purpose, need a wrapper action to make this safe",
              call: {
                action: "storage.createWithStream",
                params: "@body.file",
                implicitParams: false,
              },
            },
            {
              path: "/download/:id",
              method: "GET",
              call: {
                action: "storage.getURL",
                params: {
                  id: "@path.id",
                  expiryHours: "@query.expiryHours:number",
                  prompt: "@query.prompt:boolean",
                  promptAs: "@query.promptAs:string",
                },
                map: `({ response }) => ({
                  $status: response ? 303 : 404,
                  $headers: response ? { "Location":  response } : undefined,
                })`,
              },
            },
            {
              path: "/:id",
              method: "GET",
              call: {
                action: "storage.find",
                params: {
                  id: "@path.id",
                },
                map: `({ response }) => ({
                  $status: response ? 200 : 404,
                  $body: response,
                })`,
              },
            },
            {
              path: "/:id",
              method: "PUT",
              call: {
                action: "storage.update",
                params: {
                  id: "@path.id",
                  // id, name, tags, private, contentType
                },
              },
            },
            {
              path: "/:id",
              method: "DELETE",
              call: {
                action: "storage.delete",
                params: {
                  id: "@path.id",
                },
              },
            },
          ],
        },
      },
    }
    {
      branch: "master",
      policy: {},
      protocol: {
        GraphQL: {
          typeDefs: `
            extend type Query {
              storage: StorageQuery!
            }
        
            type StorageQuery {
              files(offset: Int = 0, limit: Int = 10): FileList!
              file(id: ID!): File
            }
        
            type File {
              id: ID!
              name: String!
              contentType: String!
              tags: JSON!
              private: Boolean!
              byteSize: Int!
              size: String!
              url(expiryHours: Int = 2, prompt: Boolean = false, promptAs: String): String!
              updatedAt: DateTime!
              createdAt: DateTime!
            }
        
            type FileList {
              offset: Int!
              limit: Int!
        
              # it is not exact value
              total: Int!
              entries: [File!]!
              hasNext: Boolean!
              hasPrev: Boolean!
            }
        
            extend type Mutation {
              storage: StorageMutation!
            }
        
            type StorageMutation {
              upload(file: Upload!): File!
              update(id: ID!, name: String, tags: JSON, private: Boolean): File!
              delete(id: ID!): Boolean!
            }
          `,
          resolvers: {
            Query: {
              storage: `() => ({})`,
            },
            Mutation: {
              storage: `() => ({})`,
            },
            StorageMutation: {
              upload: {
                call: {
                  action: "storage.create",
                  params: {
                    file: "@args.file",
                    meta: {
                      allowedContentTypes: [],
                      private: false,
                    },
                  },
                },
              },
              update: {
                call: {
                  action: "storage.update",
                  params: {},
                },
              },
              delete: {
                call: {
                  action: "storage.delete",
                  params: {},
                }
              },
            },
            StorageQuery: {
              file: {
                call: {
                  action: "storage.find",
                  params: {
                    id: "@args.id[]",
                  },
                },
              },
              files: {
                call: {
                  action: "storage.get",
                  params: {
                    offset: "@args.offset",
                    limit: "@args.limit",
                  },
                },
              },
            },
            File: {
              url: {
                call: {
                  action: "storage.getURL",
                  params: {
                    id: "@source.id[]",
                    expiryHours: "@args.expiryHours",
                    prompt: "@args.prompt",
                    promptAs: "@args.promptAs",
                  },
                },
              },
              size: (({ source }: any) => {
                const { byteSize = 0 } = source;
                const boundary = 1000;
                if (byteSize < boundary) {
                  return `${byteSize} B`;
                }
                let div = boundary;
                let exp = 0;
                for (let n = byteSize / boundary; n >= boundary; n /= boundary) {
                  div *= boundary;
                  exp++;
                }
                const size = byteSize / div;
                return `${isNaN(size) ? "-" : size.toLocaleString()} ${"KMGTPE"[exp]}B`;
              }).toString(),
            },
            FileList: {
              hasPrev: `({ source }) => source.offset > 0`,
            }
          },
        },
      },
    }
    {
      branch: "master",
      policy: {
        call: [
          {
            actions: ["user.update"],
            description: "A user can update the user profile which is belongs to own account.",
            scope: ["user.write"],
            filter: (({ context, params }) => {
              console.log(context);
              if (params.id) {
                return context.auth.identity.sub === params.id;
              }
              return false;
            }).toString(),
          },
        ],
      },
      protocol: {
        GraphQL: {
          typeDefs: `
            extend type Query {
              viewer: User
              user(id: Int, identityId: String, username: String, where: JSON): User
            }
      
            extend type Mutation {
              createUser(input: UserInput!): User!
              updateUser(input: UserInput!): User!
            }
      
            input SportsUserInput {
              id: Int
              username: String
              birthdate: Date
              gender: Gender
              name: String
              pictureFileId: String
            }
            
            type User {
              id: Int!
              username: String!
              name: String!
              birthdate: Date
              gender: Gender
              pictureFile: File
            }
          `,
          resolvers: {
            User: {
              pictureFile: {
                call: {
                  if: "({ source }) => !!source.pictureFileId",
                  action: "storage.find",
                  params: {
                    id: "@source.pictureFileId[]",
                  },
                },
              },
            },
            Mutation: {
              createUser: {
                call: {
                  action: "user.create",
                  params: "@args.input",
                  implicitParams: false,
                },
              },
              updateUser: {
                call: {
                  action: "user.update",
                  params: "@args.input",
                  implicitParams: false,
                },
              },
            },
          },
        },
      },
    }
    {
      branch: "master",
      policy: {},
      protocol: {
        REST: {
          description: "update user's FCM registration token",
          basePath: "/notification",
          routes: [
            {
              method: "PUT",
              path: "/update-token",
              call: {
                action: "notification.updateToken",
                params: {
                  identityId: "@context.auth.identity.sub",
                  token: "@body.token",
                },
              },
            },
          ],
        },
      },
    }
    {
      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",
                },
              },
            },
          ],
        },
      },
    }
    Build Status
    Coverage Status
    David
    Known Vulnerabilities
    NPM version
    Moleculer