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 ์Šคํ‚ค๋งˆ ์˜ˆ์‹œ

{
  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",
            },
          },
        },
      ],
    },
  },
}

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

Gateway์˜ ๋ฌด์ค‘๋‹จ ์—”๋“œํฌ์ธํŠธ ์—…๋ฐ์ดํŠธ ๋ฐ ๋ณด๊ณ 

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

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

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

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

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

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

    • 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 ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด

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

License

The project is available under the MIT license.

Build Status
Coverage Status
David
Known Vulnerabilities
NPM version
Moleculer