@canmingir/link-express 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.env.test +3 -0
  2. package/.eslintrc.js +20 -0
  3. package/.gitattributes +1 -0
  4. package/.github/workflows/publish.yml +9 -0
  5. package/README.md +1 -0
  6. package/index.js +1 -0
  7. package/package.json +51 -0
  8. package/prepare.js +15 -0
  9. package/src/authorization.js +102 -0
  10. package/src/config.js +20 -0
  11. package/src/dynamodb.js +18 -0
  12. package/src/error.js +46 -0
  13. package/src/express.js +58 -0
  14. package/src/lib/settings.js +36 -0
  15. package/src/lib/test.js +58 -0
  16. package/src/models/Organization.js +17 -0
  17. package/src/models/Permission.js +33 -0
  18. package/src/models/Project.js +37 -0
  19. package/src/models/Settings.js +21 -0
  20. package/src/models/index.js +51 -0
  21. package/src/openapi.js +40 -0
  22. package/src/platform.js +46 -0
  23. package/src/postgres.js +211 -0
  24. package/src/routes/index.js +15 -0
  25. package/src/routes/metrics.js +12 -0
  26. package/src/routes/oauth.js +100 -0
  27. package/src/routes/organizations.js +42 -0
  28. package/src/routes/permissions.js +50 -0
  29. package/src/routes/projects.js +125 -0
  30. package/src/routes/settings.js +25 -0
  31. package/src/routes/test/oauth.spec.js +113 -0
  32. package/src/routes/test/permissions.spec.js +154 -0
  33. package/src/routes/test/projects.spec.js +186 -0
  34. package/src/routes/test/settings.spec.js +40 -0
  35. package/src/schemas/Organization.js +13 -0
  36. package/src/schemas/Permission.js +20 -0
  37. package/src/schemas/Project.js +14 -0
  38. package/src/schemas/index.js +5 -0
  39. package/src/seeds/Organization.json +17 -0
  40. package/src/seeds/Permission.json +165 -0
  41. package/src/seeds/Project.json +42 -0
  42. package/src/seeds/Settings.json +25 -0
  43. package/src/test.js +105 -0
@@ -0,0 +1,113 @@
1
+ const test = require("../../lib/test");
2
+
3
+ const platform = require("../../platform");
4
+ const app = platform.express();
5
+
6
+ const request = require("supertest");
7
+ const jwt = require("jsonwebtoken");
8
+ const { equal } = require("assert");
9
+ const config = require("../../config");
10
+ const { project } = config();
11
+
12
+ const axios = require("axios");
13
+ const MockAdapter = require("axios-mock-adapter");
14
+ const mock = new MockAdapter(axios);
15
+
16
+ describe("OAuth", () => {
17
+ beforeEach(async () => {
18
+ await test.reset();
19
+ });
20
+
21
+ it("returns accessToken and refreshToken with code", async () => {
22
+ mock
23
+ .onPost(project.oauth.tokenUrl)
24
+ .reply(
25
+ 200,
26
+ "access_token=c9Q2KuluvCGdM4YZiUnGWxImvuFnbv&scope=user&token_type=bearer"
27
+ );
28
+
29
+ mock.onGet(project.oauth.userUrl).reply(200, { email: "1001" });
30
+
31
+ const {
32
+ body: { accessToken, refreshToken },
33
+ } = await request(app)
34
+ .post("/oauth")
35
+ .send({
36
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
37
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
38
+ redirectUri: project.oauth.redirectUri,
39
+ code: "vImIDQtMVcYnUCI3Brp6",
40
+ })
41
+ .expect(200);
42
+
43
+ const payload = jwt.decode(accessToken);
44
+
45
+ equal(payload.iss, "nuc");
46
+ equal(payload.aud, "cb16e069-6214-47f1-9922-1f7fe7629525");
47
+ equal(payload.sub, "1001");
48
+ equal(payload.rls, "OWNER");
49
+ equal(payload.aid, "977f5f57-8936-4388-8eb0-00a512cf01cc");
50
+ equal(payload.oid, "dfb990bb-81dd-4584-82ce-050eb8f6a12f");
51
+ equal(refreshToken, "c9Q2KuluvCGdM4YZiUnGWxImvuFnbv");
52
+ });
53
+
54
+ it("returns accessToken and refreshToken with refresh token", async () => {
55
+ mock
56
+ .onGet(project.oauth.userUrl)
57
+ .reply(200, { email: "liam@rebellioncoffee.shop" });
58
+
59
+ const {
60
+ body: { accessToken, refreshToken },
61
+ } = await request(app)
62
+ .post("/oauth")
63
+ .send({
64
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
65
+ refreshToken: "lzk7FZGga5hHrfiAePtswijiJHIOev",
66
+ })
67
+ .expect(200);
68
+
69
+ const payload = jwt.decode(accessToken);
70
+
71
+ equal(payload.sub, "liam@rebellioncoffee.shop");
72
+ equal(payload.iss, "nuc");
73
+ equal(refreshToken, "lzk7FZGga5hHrfiAePtswijiJHIOev");
74
+ });
75
+
76
+ it("returns 400 if code and refreshToken are missing", async () => {
77
+ await request(app).post("/oauth").send({}).expect(400);
78
+ });
79
+
80
+ it("returns 401 if code is invalid", async () => {
81
+ mock
82
+ .onPost(project.oauth.tokenUrl)
83
+ .reply(200, "error=bad_verification_code");
84
+ mock.onGet(project.oauth.userUrl).reply(401);
85
+
86
+ const res = await request(app).post("/oauth").send({
87
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
88
+ code: "ZpodRqsLu2EJxbVrcqnV",
89
+ });
90
+ equal(res.status, 401);
91
+ });
92
+
93
+ it("returns 503 if OAuth Provider is not accessible", async () => {
94
+ mock.onPost(project.oauth.tokenUrl).networkError();
95
+ mock.onGet(project.oauth.userUrl).networkError();
96
+
97
+ await request(app)
98
+ .post("/oauth")
99
+ .send({
100
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
101
+ refreshToken: "WnhGHF55s6HFRgpRL9AcV2N2VcYemj",
102
+ })
103
+ .expect(503);
104
+
105
+ await request(app)
106
+ .post("/oauth")
107
+ .send({
108
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
109
+ code: "RwlaK2waOdbAa4tt19RF",
110
+ })
111
+ .expect(503);
112
+ });
113
+ });
@@ -0,0 +1,154 @@
1
+ const test = require("../../lib/test");
2
+
3
+ const platform = require("../../platform");
4
+ const app = platform.express();
5
+
6
+ const request = require("supertest");
7
+ const { deepEqual } = require("assert");
8
+
9
+ describe("Permissions", () => {
10
+ beforeEach(async () => {
11
+ await test.reset();
12
+ });
13
+
14
+ it("creates permission", async () => {
15
+ const {
16
+ body: { id },
17
+ } = await request(app)
18
+ .post("/permissions")
19
+ .send({
20
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
21
+ organizationId: "1c063446-7e78-432a-a273-34f481d0f0c3",
22
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
23
+ userId: "marcus@nucleoidai.com",
24
+ role: "OWNER",
25
+ })
26
+ .expect(201);
27
+
28
+ const { body: permission } = await request(app)
29
+ .get(`/permissions`)
30
+ .expect(200);
31
+
32
+ deepEqual(permission, [
33
+ {
34
+ id: "e81887da-d05f-4959-9def-6cd137857088",
35
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
36
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
37
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
38
+ userId: "1001",
39
+ role: "OWNER",
40
+ },
41
+ {
42
+ id: "a1b60c53-66e2-4034-8654-38b83577f279",
43
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
44
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
45
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
46
+ userId: "49736917",
47
+ role: "OWNER",
48
+ },
49
+ {
50
+ id: "38b1cc0c-46d8-4f95-9be9-0df1a6c91d7b",
51
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
52
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
53
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
54
+ userId: "54210920",
55
+ role: "OWNER",
56
+ },
57
+ {
58
+ id: "7f7bd2c8-3c27-455c-814e-1874e96246ed",
59
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
60
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
61
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
62
+ userId: "90180086",
63
+ role: "OWNER",
64
+ },
65
+
66
+ {
67
+ id,
68
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
69
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
70
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
71
+ userId: "marcus@nucleoidai.com",
72
+ role: "ADMIN",
73
+ },
74
+ ]);
75
+ });
76
+
77
+ it("lists permissions", async () => {
78
+ const { body: permissions } = await request(app)
79
+ .get("/permissions")
80
+ .expect(200);
81
+
82
+ deepEqual(permissions, [
83
+ {
84
+ id: "e81887da-d05f-4959-9def-6cd137857088",
85
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
86
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
87
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
88
+ userId: "1001",
89
+ role: "OWNER",
90
+ },
91
+ {
92
+ id: "a1b60c53-66e2-4034-8654-38b83577f279",
93
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
94
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
95
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
96
+ userId: "49736917",
97
+ role: "OWNER",
98
+ },
99
+ {
100
+ id: "38b1cc0c-46d8-4f95-9be9-0df1a6c91d7b",
101
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
102
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
103
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
104
+ userId: "54210920",
105
+ role: "OWNER",
106
+ },
107
+ {
108
+ id: "7f7bd2c8-3c27-455c-814e-1874e96246ed",
109
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
110
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
111
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
112
+ userId: "90180086",
113
+ role: "OWNER",
114
+ },
115
+ ]);
116
+ });
117
+
118
+ it("deletes permission by userId", async () => {
119
+ await request(app).delete(`/permissions/1001`).expect(204);
120
+
121
+ await request(app).get("/permissions/1001").expect(404);
122
+
123
+ const { body: permissions } = await request(app)
124
+ .get("/permissions")
125
+ .expect(200);
126
+
127
+ deepEqual(permissions, [
128
+ {
129
+ id: "a1b60c53-66e2-4034-8654-38b83577f279",
130
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
131
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
132
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
133
+ userId: "49736917",
134
+ role: "OWNER",
135
+ },
136
+ {
137
+ id: "38b1cc0c-46d8-4f95-9be9-0df1a6c91d7b",
138
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
139
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
140
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
141
+ userId: "54210920",
142
+ role: "OWNER",
143
+ },
144
+ {
145
+ id: "7f7bd2c8-3c27-455c-814e-1874e96246ed",
146
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
147
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
148
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
149
+ userId: "90180086",
150
+ role: "OWNER",
151
+ },
152
+ ]);
153
+ });
154
+ });
@@ -0,0 +1,186 @@
1
+ const test = require("../../lib/test");
2
+
3
+ const platform = require("../../platform");
4
+ const app = platform.express();
5
+
6
+ const request = require("supertest");
7
+ const { deepEqual } = require("assert");
8
+
9
+ describe("Project", () => {
10
+ beforeEach(async () => {
11
+ await test.reset();
12
+ });
13
+
14
+ it("create project", async () => {
15
+ const { body: project } = await request(app)
16
+ .post("/projects")
17
+ .send({
18
+ name: "NEW Rebellion Coffee Shop",
19
+ description:
20
+ "NEW The finest cantina-style coffee shop this side of Mos Eisley, serving premium caf brewed from beans grown in the forests of Kashyyyk.",
21
+ icon: ":NEWcoffee:",
22
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
23
+ })
24
+ .expect(201);
25
+
26
+ const { body: projects } = await request(app).get(`/projects`).expect(200);
27
+
28
+ deepEqual(projects, [
29
+ {
30
+ id: "cb16e069-6214-47f1-9922-1f7fe7629525",
31
+ name: "Rebellion Coffee Shop",
32
+ icon: ":ph:coffee-bean-duotone:",
33
+ description:
34
+ "The finest cantina-style coffee shop this side of Mos Eisley, serving premium caf brewed from beans grown in the forests of Kashyyyk.",
35
+ type: "SINGLE",
36
+ coach: null,
37
+ organization: {
38
+ id: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
39
+ name: "Rebellion Coffee Shop",
40
+ },
41
+ },
42
+ {
43
+ id: "0c756054-2d28-4f87-9b12-8023a79136a5",
44
+ name: "Good Galactic Corp.",
45
+ icon: ":fa6-brands:galactic-republic:",
46
+ description:
47
+ "The most trusted Galacticing institution in the Core Worlds, with secure vaults that would impress even the Empire.",
48
+ type: "MULTI",
49
+ coach: null,
50
+ organization: {
51
+ id: "1c063446-7e78-432a-a273-34f481d0f0c3",
52
+ name: "Good Galactic Corp.",
53
+ },
54
+ },
55
+ {
56
+ id: "21d2530b-4657-4ac0-b8cd-1a9f82786e32",
57
+ name: "Fire Logistics",
58
+ icon: ":ph:fire-simple-duotone:",
59
+ description:
60
+ "The fastest cargo haulers in the Outer Rim, making the Kessel Run in under twelve parsecs.",
61
+ type: "SINGLE",
62
+ coach: null,
63
+ organization: {
64
+ id: "5459ab03-204a-4627-bdde-667b7802cb35",
65
+ name: "Fire Logistics",
66
+ },
67
+ },
68
+ {
69
+ id: project.id,
70
+ name: "NEW Rebellion Coffee Shop",
71
+ icon: ":NEWcoffee:",
72
+ description:
73
+ "NEW The finest cantina-style coffee shop this side of Mos Eisley, serving premium caf brewed from beans grown in the forests of Kashyyyk.",
74
+ type: null,
75
+ coach: null,
76
+ organization: {
77
+ id: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
78
+ name: "Rebellion Coffee Shop",
79
+ },
80
+ },
81
+ ]);
82
+ });
83
+
84
+ it("lists projects", async () => {
85
+ const { body: res } = await request(app).get("/projects").expect(200);
86
+ console.log(res);
87
+
88
+ deepEqual(res, [
89
+ {
90
+ id: "cb16e069-6214-47f1-9922-1f7fe7629525",
91
+ name: "Rebellion Coffee Shop",
92
+ icon: ":ph:coffee-bean-duotone:",
93
+ description:
94
+ "The finest cantina-style coffee shop this side of Mos Eisley, serving premium caf brewed from beans grown in the forests of Kashyyyk.",
95
+ type: "SINGLE",
96
+ coach: null,
97
+ organization: {
98
+ id: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
99
+ name: "Rebellion Coffee Shop",
100
+ },
101
+ },
102
+ {
103
+ id: "0c756054-2d28-4f87-9b12-8023a79136a5",
104
+ name: "Good Galactic Corp.",
105
+ icon: ":fa6-brands:galactic-republic:",
106
+ description:
107
+ "The most trusted Galacticing institution in the Core Worlds, with secure vaults that would impress even the Empire.",
108
+ type: "MULTI",
109
+ coach: null,
110
+ organization: {
111
+ id: "1c063446-7e78-432a-a273-34f481d0f0c3",
112
+ name: "Good Galactic Corp.",
113
+ },
114
+ },
115
+ {
116
+ id: "21d2530b-4657-4ac0-b8cd-1a9f82786e32",
117
+ name: "Fire Logistics",
118
+ icon: ":ph:fire-simple-duotone:",
119
+ description:
120
+ "The fastest cargo haulers in the Outer Rim, making the Kessel Run in under twelve parsecs.",
121
+ type: "SINGLE",
122
+ coach: null,
123
+ organization: {
124
+ id: "5459ab03-204a-4627-bdde-667b7802cb35",
125
+ name: "Fire Logistics",
126
+ },
127
+ },
128
+ ]);
129
+ });
130
+
131
+ it("get project by id", async () => {
132
+ const { body: res } = await request(app)
133
+ .get("/projects/add6dfa4-45ba-4da2-bc5c-5a529610b52f")
134
+ .expect(200);
135
+ deepEqual(res, {
136
+ id: "add6dfa4-45ba-4da2-bc5c-5a529610b52f",
137
+ name: "Rebellion Coffee Shop Team",
138
+ coach: "Elijah",
139
+ icon: ":ph:coffee-bean-duotone:",
140
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
141
+ type: null,
142
+ description: null,
143
+ });
144
+ });
145
+
146
+ it("get project by id forbidden", async () => {
147
+ await request(app)
148
+ .get("/projects/11d2530b-4657-4ac0-b8cd-1a9f82786e32")
149
+ .expect(404);
150
+ });
151
+
152
+ it("update project", async () => {
153
+ await request(app)
154
+ .patch("/projects/add6dfa4-45ba-4da2-bc5c-5a529610b52f")
155
+ .send({
156
+ name: "Updated Rebellion Coffee Shop Team",
157
+ coach: "updated Elijah",
158
+ icon: ":NEWbeans:",
159
+ })
160
+ .expect(200);
161
+
162
+ const { body: updatedProject } = await request(app)
163
+ .get(`/projects/add6dfa4-45ba-4da2-bc5c-5a529610b52f`)
164
+ .expect(200);
165
+
166
+ deepEqual(updatedProject, {
167
+ id: "add6dfa4-45ba-4da2-bc5c-5a529610b52f",
168
+ name: "Updated Rebellion Coffee Shop Team",
169
+ coach: "updated Elijah",
170
+ icon: ":NEWbeans:",
171
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
172
+ type: null,
173
+ description: null,
174
+ });
175
+ });
176
+
177
+ it("delete project", async () => {
178
+ await request(app)
179
+ .delete("/projects/add6dfa4-45ba-4da2-bc5c-5a529610b52f")
180
+ .expect(204);
181
+
182
+ await request(app)
183
+ .get(`/projects/add6dfa4-45ba-4da2-bc5c-5a529610b52f`)
184
+ .expect(404);
185
+ });
186
+ });
@@ -0,0 +1,40 @@
1
+ const test = require("../../lib/test");
2
+
3
+ const platform = require("../../platform");
4
+ const app = platform.express();
5
+
6
+ const request = require("supertest");
7
+ const { deepEqual } = require("assert");
8
+
9
+ describe("Setting", () => {
10
+ beforeEach(async () => {
11
+ await test.reset();
12
+ });
13
+
14
+ it("get settings by project id", async () => {
15
+ const { body: res } = await request(app)
16
+ .get("/projects/cb16e069-6214-47f1-9922-1f7fe7629525/settings")
17
+ .expect(200);
18
+
19
+ deepEqual(res, {
20
+ timeZone: "America/New_York",
21
+ });
22
+ });
23
+
24
+ it("updates settings by project id", async () => {
25
+ await request(app)
26
+ .patch("/projects/cb16e069-6214-47f1-9922-1f7fe7629525/settings")
27
+ .send({
28
+ timeZone: "America/Los_Angeles",
29
+ })
30
+ .expect(200);
31
+
32
+ const { body: res } = await request(app)
33
+ .get("/projects/cb16e069-6214-47f1-9922-1f7fe7629525/settings")
34
+ .expect(200);
35
+
36
+ deepEqual(res, {
37
+ timeZone: "America/Los_Angeles",
38
+ });
39
+ });
40
+ });
@@ -0,0 +1,13 @@
1
+ const Joi = require("joi");
2
+
3
+ const Organization = {
4
+ create: Joi.object({
5
+ name: Joi.string().required(),
6
+ }).required(),
7
+ list: Joi.object({
8
+ id: Joi.string().guid({ version: "uuidv4" }),
9
+ name: Joi.string(),
10
+ }).required(),
11
+ };
12
+
13
+ module.exports = Organization;
@@ -0,0 +1,20 @@
1
+ const Joi = require("joi");
2
+
3
+ const Permission = {
4
+ create: Joi.object({
5
+ appId: Joi.string().required(),
6
+ organizationId: Joi.string().required(),
7
+ projectId: Joi.string().guid().required(),
8
+ userId: Joi.string().required(),
9
+ role: Joi.string().required(),
10
+ }).required(),
11
+ list: Joi.object({
12
+ appId: Joi.string().required(),
13
+ organizationId: Joi.string().required(),
14
+ projectId: Joi.string().guid().required(),
15
+ userId: Joi.string().required(),
16
+ role: Joi.string().optional(),
17
+ }).required(),
18
+ };
19
+
20
+ module.exports = Permission;
@@ -0,0 +1,14 @@
1
+ const Joi = require("joi");
2
+
3
+ const Project = Joi.object({
4
+ name: Joi.string().required(),
5
+ icon: Joi.string().required(),
6
+ description: Joi.string().optional(),
7
+ type: Joi.string().optional(),
8
+ organizationId: Joi.string()
9
+ .guid({ version: ["uuidv4"] })
10
+ .optional(),
11
+ coach: Joi.string().optional(),
12
+ });
13
+
14
+ module.exports = Project;
@@ -0,0 +1,5 @@
1
+ const Permission = require("./Permission");
2
+ const Organization = require("./Organization");
3
+ const Project = require("./Project");
4
+
5
+ module.exports = { Permission, Organization, Project };
@@ -0,0 +1,17 @@
1
+ {
2
+ "sequence": 0,
3
+ "seed": [
4
+ {
5
+ "id": "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
6
+ "name": "Rebellion Coffee Shop"
7
+ },
8
+ {
9
+ "id": "1c063446-7e78-432a-a273-34f481d0f0c3",
10
+ "name": "Good Galactic Corp."
11
+ },
12
+ {
13
+ "id": "5459ab03-204a-4627-bdde-667b7802cb35",
14
+ "name": "Fire Logistics"
15
+ }
16
+ ]
17
+ }