Tataaicoltd commited on
Commit
9b40bfd
·
verified ·
1 Parent(s): 0a786d4

feat: add Swagger UI at /api-docs

Browse files
Files changed (4) hide show
  1. package-lock.json +31 -2
  2. package.json +2 -1
  3. server.js +8 -0
  4. src/openapi.js +405 -0
package-lock.json CHANGED
@@ -12,12 +12,19 @@
12
  "cors": "^2.8.5",
13
  "dotenv": "^16.4.5",
14
  "express": "^4.19.2",
15
- "multer": "^2.0.1"
 
16
  },
17
  "engines": {
18
- "node": ">=18"
19
  }
20
  },
 
 
 
 
 
 
21
  "node_modules/@supabase/auth-js": {
22
  "version": "2.110.6",
23
  "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.110.6.tgz",
@@ -921,6 +928,28 @@
921
  "safe-buffer": "~5.2.0"
922
  }
923
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
924
  "node_modules/toidentifier": {
925
  "version": "1.0.1",
926
  "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
 
12
  "cors": "^2.8.5",
13
  "dotenv": "^16.4.5",
14
  "express": "^4.19.2",
15
+ "multer": "^2.0.1",
16
+ "swagger-ui-express": "^5.0.1"
17
  },
18
  "engines": {
19
+ "node": ">=22"
20
  }
21
  },
22
+ "node_modules/@scarf/scarf": {
23
+ "version": "1.4.0",
24
+ "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
25
+ "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
26
+ "hasInstallScript": true
27
+ },
28
  "node_modules/@supabase/auth-js": {
29
  "version": "2.110.6",
30
  "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.110.6.tgz",
 
928
  "safe-buffer": "~5.2.0"
929
  }
930
  },
931
+ "node_modules/swagger-ui-dist": {
932
+ "version": "5.32.8",
933
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz",
934
+ "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==",
935
+ "dependencies": {
936
+ "@scarf/scarf": "=1.4.0"
937
+ }
938
+ },
939
+ "node_modules/swagger-ui-express": {
940
+ "version": "5.0.1",
941
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
942
+ "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
943
+ "dependencies": {
944
+ "swagger-ui-dist": ">=5.0.0"
945
+ },
946
+ "engines": {
947
+ "node": ">= v0.10.32"
948
+ },
949
+ "peerDependencies": {
950
+ "express": ">=4.0.0 || >=5.0.0-beta"
951
+ }
952
+ },
953
  "node_modules/toidentifier": {
954
  "version": "1.0.1",
955
  "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
package.json CHANGED
@@ -16,6 +16,7 @@
16
  "cors": "^2.8.5",
17
  "dotenv": "^16.4.5",
18
  "express": "^4.19.2",
19
- "multer": "^2.0.1"
 
20
  }
21
  }
 
16
  "cors": "^2.8.5",
17
  "dotenv": "^16.4.5",
18
  "express": "^4.19.2",
19
+ "multer": "^2.0.1",
20
+ "swagger-ui-express": "^5.0.1"
21
  }
22
  }
server.js CHANGED
@@ -3,10 +3,12 @@ import cors from 'cors';
3
  import { fileURLToPath } from 'url';
4
  import path from 'path';
5
  import dotenv from 'dotenv';
 
6
 
7
  import projectsRouter from './src/routes/projects.js';
8
  import ticketsRouter from './src/routes/tickets.js';
9
  import flowRouter from './src/routes/flow.js';
 
10
 
11
  dotenv.config();
12
 
@@ -26,6 +28,12 @@ app.use('/flow/tickets', flowRouter);
26
 
27
  app.get('/api/health', (req, res) => res.json({ ok: true }));
28
 
 
 
 
 
 
 
29
  // Static frontend
30
  app.use(express.static(path.join(__dirname, 'public')));
31
 
 
3
  import { fileURLToPath } from 'url';
4
  import path from 'path';
5
  import dotenv from 'dotenv';
6
+ import swaggerUi from 'swagger-ui-express';
7
 
8
  import projectsRouter from './src/routes/projects.js';
9
  import ticketsRouter from './src/routes/tickets.js';
10
  import flowRouter from './src/routes/flow.js';
11
+ import { openapiSpec } from './src/openapi.js';
12
 
13
  dotenv.config();
14
 
 
28
 
29
  app.get('/api/health', (req, res) => res.json({ ok: true }));
30
 
31
+ // API documentation (Swagger UI) + raw OpenAPI spec
32
+ app.get('/api-docs.json', (req, res) => res.json(openapiSpec));
33
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpec, {
34
+ customSiteTitle: 'Taboard API Docs'
35
+ }));
36
+
37
  // Static frontend
38
  app.use(express.static(path.join(__dirname, 'public')));
39
 
src/openapi.js ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // OpenAPI 3.0 specification for the Taboard API, served via Swagger UI at /api-docs.
2
+ const STATUSES = ['open', 'queued', 'processing', 'done', 'failed', 'close'];
3
+ const TYPES = ['backend', 'api', 'frontend', 'ui'];
4
+
5
+ export const openapiSpec = {
6
+ openapi: '3.0.3',
7
+ info: {
8
+ title: 'Taboard API',
9
+ version: '1.0.0',
10
+ description: 'Jira-style admin board + API backed by Supabase.'
11
+ },
12
+ tags: [
13
+ { name: 'Health', description: 'Service health' },
14
+ { name: 'Projects', description: 'Project management' },
15
+ { name: 'Tickets', description: 'Ticket management (board)' },
16
+ { name: 'Flow', description: 'Lightweight ticket flow API for agents' }
17
+ ],
18
+ components: {
19
+ schemas: {
20
+ Error: {
21
+ type: 'object',
22
+ properties: { error: { type: 'string' } }
23
+ },
24
+ Project: {
25
+ type: 'object',
26
+ properties: {
27
+ id: { type: 'integer', readOnly: true },
28
+ name: { type: 'string' },
29
+ note: { type: 'string', nullable: true },
30
+ struc: { type: 'object', additionalProperties: true }
31
+ }
32
+ },
33
+ ProjectInput: {
34
+ type: 'object',
35
+ required: ['name'],
36
+ properties: {
37
+ name: { type: 'string' },
38
+ note: { type: 'string', nullable: true },
39
+ struc: {
40
+ oneOf: [{ type: 'object' }, { type: 'string' }],
41
+ description: 'Object, or a JSON string (parsed server-side).'
42
+ }
43
+ }
44
+ },
45
+ Ticket: {
46
+ type: 'object',
47
+ properties: {
48
+ id: { type: 'integer', readOnly: true },
49
+ title: { type: 'string' },
50
+ detail: { type: 'string', nullable: true },
51
+ type: { type: 'string', enum: TYPES },
52
+ status: { type: 'string', enum: STATUSES },
53
+ message: { type: 'string', nullable: true },
54
+ logs: { type: 'string', nullable: true },
55
+ sort: { type: 'integer' },
56
+ subtic: { type: 'integer', nullable: true },
57
+ projectid: { type: 'integer', nullable: true },
58
+ images: { type: 'array', items: { type: 'string', format: 'uri' } }
59
+ }
60
+ },
61
+ TicketInput: {
62
+ type: 'object',
63
+ required: ['title'],
64
+ properties: {
65
+ title: { type: 'string' },
66
+ detail: { type: 'string', nullable: true },
67
+ type: { type: 'string', enum: TYPES, default: 'backend' },
68
+ status: { type: 'string', enum: STATUSES, default: 'open' },
69
+ message: { type: 'string', nullable: true },
70
+ logs: { type: 'string', nullable: true },
71
+ sort: { type: 'integer', default: 0 },
72
+ subtic: { type: 'integer', nullable: true },
73
+ projectid: { type: 'integer', nullable: true }
74
+ }
75
+ },
76
+ FlowTicket: {
77
+ type: 'object',
78
+ properties: {
79
+ id: { type: 'integer' },
80
+ title: { type: 'string' },
81
+ detail: { type: 'string', nullable: true },
82
+ status: { type: 'string', enum: STATUSES },
83
+ message: { type: 'string', nullable: true }
84
+ }
85
+ },
86
+ FlowUpdate: {
87
+ type: 'object',
88
+ required: ['id'],
89
+ properties: {
90
+ id: { type: 'integer' },
91
+ status: { type: 'string', enum: STATUSES },
92
+ message: { type: 'string' },
93
+ detail: { type: 'string' }
94
+ }
95
+ }
96
+ },
97
+ responses: {
98
+ NotFound: {
99
+ description: 'Not found',
100
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }
101
+ },
102
+ BadRequest: {
103
+ description: 'Invalid request',
104
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }
105
+ },
106
+ ServerError: {
107
+ description: 'Server error',
108
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }
109
+ }
110
+ }
111
+ },
112
+ paths: {
113
+ '/api/health': {
114
+ get: {
115
+ tags: ['Health'],
116
+ summary: 'Health check',
117
+ responses: {
118
+ 200: {
119
+ description: 'OK',
120
+ content: {
121
+ 'application/json': {
122
+ schema: { type: 'object', properties: { ok: { type: 'boolean' } } }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ },
129
+ '/api/projects': {
130
+ get: {
131
+ tags: ['Projects'],
132
+ summary: 'List projects',
133
+ responses: {
134
+ 200: {
135
+ description: 'Array of projects',
136
+ content: {
137
+ 'application/json': {
138
+ schema: { type: 'array', items: { $ref: '#/components/schemas/Project' } }
139
+ }
140
+ }
141
+ },
142
+ 500: { $ref: '#/components/responses/ServerError' }
143
+ }
144
+ },
145
+ post: {
146
+ tags: ['Projects'],
147
+ summary: 'Create project',
148
+ requestBody: {
149
+ required: true,
150
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/ProjectInput' } } }
151
+ },
152
+ responses: {
153
+ 201: {
154
+ description: 'Created',
155
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Project' } } }
156
+ },
157
+ 400: { $ref: '#/components/responses/BadRequest' },
158
+ 500: { $ref: '#/components/responses/ServerError' }
159
+ }
160
+ }
161
+ },
162
+ '/api/projects/{id}': {
163
+ parameters: [
164
+ { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }
165
+ ],
166
+ get: {
167
+ tags: ['Projects'],
168
+ summary: 'Get project',
169
+ responses: {
170
+ 200: {
171
+ description: 'Project',
172
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Project' } } }
173
+ },
174
+ 404: { $ref: '#/components/responses/NotFound' }
175
+ }
176
+ },
177
+ put: {
178
+ tags: ['Projects'],
179
+ summary: 'Update project',
180
+ requestBody: {
181
+ required: true,
182
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/ProjectInput' } } }
183
+ },
184
+ responses: {
185
+ 200: {
186
+ description: 'Updated',
187
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Project' } } }
188
+ },
189
+ 500: { $ref: '#/components/responses/ServerError' }
190
+ }
191
+ },
192
+ delete: {
193
+ tags: ['Projects'],
194
+ summary: 'Delete project',
195
+ responses: {
196
+ 204: { description: 'Deleted' },
197
+ 500: { $ref: '#/components/responses/ServerError' }
198
+ }
199
+ }
200
+ },
201
+ '/api/tickets': {
202
+ get: {
203
+ tags: ['Tickets'],
204
+ summary: 'List tickets',
205
+ parameters: [
206
+ {
207
+ name: 'subtic', in: 'query',
208
+ schema: { type: 'string' },
209
+ description: 'Filter by parent ticket id, or "null" for top-level tickets.'
210
+ },
211
+ {
212
+ name: 'projectid', in: 'query',
213
+ schema: { type: 'integer' },
214
+ description: 'Filter by project id.'
215
+ }
216
+ ],
217
+ responses: {
218
+ 200: {
219
+ description: 'Array of tickets',
220
+ content: {
221
+ 'application/json': {
222
+ schema: { type: 'array', items: { $ref: '#/components/schemas/Ticket' } }
223
+ }
224
+ }
225
+ },
226
+ 500: { $ref: '#/components/responses/ServerError' }
227
+ }
228
+ },
229
+ post: {
230
+ tags: ['Tickets'],
231
+ summary: 'Create ticket',
232
+ requestBody: {
233
+ required: true,
234
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/TicketInput' } } }
235
+ },
236
+ responses: {
237
+ 201: {
238
+ description: 'Created',
239
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } }
240
+ },
241
+ 400: { $ref: '#/components/responses/BadRequest' },
242
+ 500: { $ref: '#/components/responses/ServerError' }
243
+ }
244
+ }
245
+ },
246
+ '/api/tickets/{id}': {
247
+ parameters: [
248
+ { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }
249
+ ],
250
+ get: {
251
+ tags: ['Tickets'],
252
+ summary: 'Get ticket',
253
+ responses: {
254
+ 200: {
255
+ description: 'Ticket',
256
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } }
257
+ },
258
+ 404: { $ref: '#/components/responses/NotFound' }
259
+ }
260
+ },
261
+ put: {
262
+ tags: ['Tickets'],
263
+ summary: 'Update ticket',
264
+ requestBody: {
265
+ required: true,
266
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/TicketInput' } } }
267
+ },
268
+ responses: {
269
+ 200: {
270
+ description: 'Updated',
271
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } }
272
+ },
273
+ 400: { $ref: '#/components/responses/BadRequest' },
274
+ 500: { $ref: '#/components/responses/ServerError' }
275
+ }
276
+ },
277
+ delete: {
278
+ tags: ['Tickets'],
279
+ summary: 'Delete ticket (sub-tickets cascade)',
280
+ responses: {
281
+ 204: { description: 'Deleted' },
282
+ 500: { $ref: '#/components/responses/ServerError' }
283
+ }
284
+ }
285
+ },
286
+ '/api/tickets/{id}/images': {
287
+ parameters: [
288
+ { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }
289
+ ],
290
+ post: {
291
+ tags: ['Tickets'],
292
+ summary: 'Upload ticket images',
293
+ requestBody: {
294
+ required: true,
295
+ content: {
296
+ 'multipart/form-data': {
297
+ schema: {
298
+ type: 'object',
299
+ properties: {
300
+ images: {
301
+ type: 'array',
302
+ items: { type: 'string', format: 'binary' }
303
+ }
304
+ }
305
+ }
306
+ }
307
+ }
308
+ },
309
+ responses: {
310
+ 201: {
311
+ description: 'Ticket with appended image URLs',
312
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } }
313
+ },
314
+ 400: { $ref: '#/components/responses/BadRequest' },
315
+ 404: { $ref: '#/components/responses/NotFound' },
316
+ 500: { $ref: '#/components/responses/ServerError' }
317
+ }
318
+ },
319
+ delete: {
320
+ tags: ['Tickets'],
321
+ summary: 'Remove one image URL from a ticket',
322
+ requestBody: {
323
+ required: true,
324
+ content: {
325
+ 'application/json': {
326
+ schema: {
327
+ type: 'object',
328
+ required: ['url'],
329
+ properties: { url: { type: 'string', format: 'uri' } }
330
+ }
331
+ }
332
+ }
333
+ },
334
+ responses: {
335
+ 200: {
336
+ description: 'Updated ticket',
337
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } }
338
+ },
339
+ 400: { $ref: '#/components/responses/BadRequest' },
340
+ 404: { $ref: '#/components/responses/NotFound' },
341
+ 500: { $ref: '#/components/responses/ServerError' }
342
+ }
343
+ }
344
+ },
345
+ '/flow/tickets': {
346
+ get: {
347
+ tags: ['Flow'],
348
+ summary: 'List tickets (id, title, status)',
349
+ responses: {
350
+ 200: {
351
+ description: 'Array of flow tickets',
352
+ content: {
353
+ 'application/json': {
354
+ schema: {
355
+ type: 'array',
356
+ items: {
357
+ type: 'object',
358
+ properties: {
359
+ id: { type: 'integer' },
360
+ title: { type: 'string' },
361
+ status: { type: 'string', enum: STATUSES }
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+ },
368
+ 500: { $ref: '#/components/responses/ServerError' }
369
+ }
370
+ },
371
+ put: {
372
+ tags: ['Flow'],
373
+ summary: 'Update ticket status/message/detail',
374
+ requestBody: {
375
+ required: true,
376
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/FlowUpdate' } } }
377
+ },
378
+ responses: {
379
+ 200: {
380
+ description: 'Updated ticket',
381
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } } }
382
+ },
383
+ 400: { $ref: '#/components/responses/BadRequest' },
384
+ 500: { $ref: '#/components/responses/ServerError' }
385
+ }
386
+ }
387
+ },
388
+ '/flow/tickets/{id}': {
389
+ parameters: [
390
+ { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }
391
+ ],
392
+ get: {
393
+ tags: ['Flow'],
394
+ summary: 'Get ticket (id, title, detail, status, message)',
395
+ responses: {
396
+ 200: {
397
+ description: 'Flow ticket',
398
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/FlowTicket' } } }
399
+ },
400
+ 404: { $ref: '#/components/responses/NotFound' }
401
+ }
402
+ }
403
+ }
404
+ }
405
+ };