@kumori/aurora-backend-handler 1.0.49 → 1.0.51

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.
@@ -222,7 +222,6 @@ export const createAccount = async (account: Account, security: Security) => {
222
222
  username: account.cloudProvider.username || "",
223
223
  password: account.cloudProvider.password || "",
224
224
  region: account.cloudProvider?.region || "",
225
- project_id: account.cloudProvider?.project_id || "",
226
225
  }
227
226
  : {
228
227
  method: providerName,
@@ -334,7 +333,6 @@ export const createAccount = async (account: Account, security: Security) => {
334
333
  password: account.cloudProvider.password || "",
335
334
  region: account.cloudProvider?.region || "",
336
335
  domain: account.cloudProvider?.domain || "",
337
- project_id: account.cloudProvider?.project_id || "",
338
336
  }
339
337
  : {
340
338
  method: providerName,
@@ -613,7 +611,6 @@ export const deleteAccount = async (account: Account, security: Security) => {
613
611
  credentials: {
614
612
  username: account.cloudProvider.username,
615
613
  password: account.cloudProvider.password,
616
- project_id: account.cloudProvider.project_id || "",
617
614
  },
618
615
  },
619
616
  };
@@ -628,7 +625,6 @@ export const deleteAccount = async (account: Account, security: Security) => {
628
625
  username: account.cloudProvider.username,
629
626
  password: account.cloudProvider.password,
630
627
  domain: account.cloudProvider.domain,
631
- project_id: account.cloudProvider.project_id || "",
632
628
  },
633
629
  },
634
630
  };
@@ -994,7 +990,6 @@ export const updateAccount = async (account: Account, security: Security) => {
994
990
  username: account.cloudProvider.username || "",
995
991
  password: account.cloudProvider.password || "",
996
992
  region: account.cloudProvider?.region || "",
997
- project_id: account.cloudProvider?.project_id || "",
998
993
  }
999
994
  : {
1000
995
  method: providerName,
@@ -1104,7 +1099,6 @@ export const updateAccount = async (account: Account, security: Security) => {
1104
1099
  password: account.cloudProvider.password || "",
1105
1100
  region: account.cloudProvider?.region || "",
1106
1101
  domain: account.cloudProvider?.domain || "",
1107
- project_id: account.cloudProvider?.project_id || "",
1108
1102
  }
1109
1103
  : {
1110
1104
  method: providerName,
@@ -7,7 +7,6 @@ import {
7
7
  makeGlobalWebSocketRequest,
8
8
  } from "../websocket-manager";
9
9
  import { Link, Notification, Service } from "@kumori/aurora-interfaces";
10
- import { Revision } from "@kumori/aurora-interfaces/interfaces/revision-interface";
11
10
  let pendingLinks = new Map<string, Link[]>();
12
11
  /**
13
12
  * Function to deploy a service
@@ -16,13 +15,13 @@ let pendingLinks = new Map<string, Link[]>();
16
15
  export const deployService = async (data: Service, token: string) => {
17
16
  try {
18
17
  const url = new URL(
19
- `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${data.tenant}/service/${data.name}`
18
+ `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${data.tenant}/service/${data.name}`,
20
19
  );
21
20
  url.searchParams.append("dryrun", "false");
22
21
  url.searchParams.append("accept", "true");
23
22
  url.searchParams.append("wait", "30000");
24
23
  url.searchParams.append("validate", "true");
25
- if(data.dsl){
24
+ if (data.dsl) {
26
25
  url.searchParams.append("dsl", "true");
27
26
  }
28
27
  if (data.serviceData) {
@@ -33,7 +32,7 @@ export const deployService = async (data: Service, token: string) => {
33
32
  JSON.stringify({
34
33
  targetAccount: data.account,
35
34
  targetEnvironment: data.environment,
36
- })
35
+ }),
37
36
  );
38
37
  formData.append("labels", JSON.stringify({ project: data.project }));
39
38
  formData.append("comment", " ");
@@ -48,7 +47,7 @@ export const deployService = async (data: Service, token: string) => {
48
47
  const jsonResponse = await response.json();
49
48
 
50
49
  const isTimeout = jsonResponse?.events?.some(
51
- (event: any) => event.content === "_timeout_"
50
+ (event: any) => event.content === "_timeout_",
52
51
  );
53
52
 
54
53
  if (isTimeout) {
@@ -81,7 +80,7 @@ export const deployService = async (data: Service, token: string) => {
81
80
  const jsonResponse = await response.json();
82
81
 
83
82
  const isTimeout = jsonResponse?.events?.some(
84
- (event: any) => event.content === "_timeout_"
83
+ (event: any) => event.content === "_timeout_",
85
84
  );
86
85
 
87
86
  if (isTimeout) {
@@ -111,7 +110,7 @@ export const redeployService = async (data: Service) => {
111
110
  try {
112
111
  const formData = await deployServiceHelper(data);
113
112
  const url = new URL(
114
- `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${data.tenant}/service/${data.name}/revision/${data.currentRevision}`
113
+ `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${data.tenant}/service/${data.name}/revision/${data.currentRevision}`,
115
114
  );
116
115
  url.searchParams.append("dryrun", "false");
117
116
  url.searchParams.append("accept", "true");
@@ -130,7 +129,7 @@ export const redeployService = async (data: Service) => {
130
129
  const jsonResponse = await response.json();
131
130
 
132
131
  const isTimeout = jsonResponse?.events?.some(
133
- (event: any) => event.content === "_timeout_"
132
+ (event: any) => event.content === "_timeout_",
134
133
  );
135
134
 
136
135
  if (isTimeout) {
@@ -163,7 +162,7 @@ export const deleteService = async (data: Service, security: string) => {
163
162
  deleteBody,
164
163
  30000,
165
164
  "DELETE",
166
- data.name
165
+ data.name,
167
166
  );
168
167
  return response;
169
168
  } catch (err) {
@@ -188,7 +187,7 @@ export const restartService = async (data: Service, security: string) => {
188
187
  restartBody,
189
188
  30000,
190
189
  "RESTART",
191
- data.name
190
+ data.name,
192
191
  );
193
192
 
194
193
  const updatedService: Service = {
@@ -209,30 +208,51 @@ export const restartService = async (data: Service, security: string) => {
209
208
  }
210
209
  };
211
210
  const generateLinkBody = (data: Service, link: Link) => {
212
- const originInClient = data.clientChannels.find(
213
- (channel) =>
214
- channel.name === link.originChannel || channel.from === link.originChannel
215
- );
216
- const originInServer = data.serverChannels.find(
217
- (channel) =>
218
- channel.name === link.originChannel || channel.from === link.originChannel
219
- );
220
- const originInDuplex = data.duplexChannels.find(
221
- (channel) =>
222
- channel.name === link.originChannel || channel.from === link.originChannel
223
- );
224
- const targetInClient = data.clientChannels.find(
225
- (channel) =>
226
- channel.name === link.targetChannel || channel.from === link.targetChannel
227
- );
228
- const targetInServer = data.serverChannels.find(
229
- (channel) =>
230
- channel.name === link.targetChannel || channel.from === link.targetChannel
231
- );
232
- const targetInDuplex = data.duplexChannels.find(
233
- (channel) =>
234
- channel.name === link.targetChannel || channel.from === link.targetChannel
235
- );
211
+ const isOrigin = data.name === link.origin;
212
+ const isTarget = data.name === link.target;
213
+
214
+ const originInClient = isOrigin
215
+ ? data.clientChannels.find(
216
+ (channel) =>
217
+ channel.name === link.originChannel ||
218
+ channel.from === link.originChannel,
219
+ )
220
+ : undefined;
221
+ const originInServer = isOrigin
222
+ ? data.serverChannels.find(
223
+ (channel) =>
224
+ channel.name === link.originChannel ||
225
+ channel.from === link.originChannel,
226
+ )
227
+ : undefined;
228
+ const originInDuplex = isOrigin
229
+ ? data.duplexChannels.find(
230
+ (channel) =>
231
+ channel.name === link.originChannel ||
232
+ channel.from === link.originChannel,
233
+ )
234
+ : undefined;
235
+ const targetInClient = isTarget
236
+ ? data.clientChannels.find(
237
+ (channel) =>
238
+ channel.name === link.targetChannel ||
239
+ channel.from === link.targetChannel,
240
+ )
241
+ : undefined;
242
+ const targetInServer = isTarget
243
+ ? data.serverChannels.find(
244
+ (channel) =>
245
+ channel.name === link.targetChannel ||
246
+ channel.from === link.targetChannel,
247
+ )
248
+ : undefined;
249
+ const targetInDuplex = isTarget
250
+ ? data.duplexChannels.find(
251
+ (channel) =>
252
+ channel.name === link.targetChannel ||
253
+ channel.from === link.targetChannel,
254
+ )
255
+ : undefined;
236
256
 
237
257
  let linkBody;
238
258
  if (originInClient) {
@@ -291,7 +311,7 @@ const generateLinkBody = (data: Service, link: Link) => {
291
311
  };
292
312
  } else {
293
313
  console.warn(
294
- `No se encontraron canales para el enlace: origin=${link.origin}, target=${link.target}`
314
+ `No se encontraron canales para el enlace: origin=${link.origin}, target=${link.target}`,
295
315
  );
296
316
  linkBody = {
297
317
  client_tenant: data.tenant,
@@ -302,6 +322,7 @@ const generateLinkBody = (data: Service, link: Link) => {
302
322
  server_channel: link.targetChannel,
303
323
  };
304
324
  }
325
+
305
326
  return linkBody;
306
327
  };
307
328
  export const linkPendingServices = async (service: Service, token: string) => {
@@ -319,7 +340,7 @@ export const linkPendingServices = async (service: Service, token: string) => {
319
340
  linkBody,
320
341
  30000,
321
342
  "LINK",
322
- service.name
343
+ service.name,
323
344
  );
324
345
 
325
346
  const notification: Notification = {
@@ -353,7 +374,7 @@ export const linkPendingServices = async (service: Service, token: string) => {
353
374
  };
354
375
  eventHelper.notification.publish.creation(notification);
355
376
  }
356
- })
377
+ }),
357
378
  );
358
379
  }
359
380
  };
@@ -375,7 +396,7 @@ export const requestRevisionData = async (service: Service, token: string) => {
375
396
  "GET_REVISION",
376
397
  service.name,
377
398
  "service",
378
- service
399
+ service,
379
400
  );
380
401
  return response;
381
402
  } catch (err) {
@@ -402,203 +423,163 @@ export const updateService = async (
402
423
  // pendingLinks.set(data.name, newLinksToCreate);
403
424
  // await linkPendingServices(serviceWithNewLinks, token);
404
425
  // }
405
- const url = new URL(
406
- `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${data.tenant}/service/${data.name}/revision/${data.currentRevision}`,
407
- );
408
- if (data.dsl) {
409
- url.searchParams.append("dsl", "true");
410
- }
411
- if (data.serviceData) {
412
- const formData = new FormData();
413
- formData.append("type", "update-bundle");
414
- formData.append("bundle", data.serviceData);
415
- formData.append(
416
- "meta",
417
- JSON.stringify({
418
- targetAccount: data.account,
419
- targetEnvironment: data.environment,
420
- }),
421
- );
422
- formData.append("labels", JSON.stringify({ project: data.project }));
423
- formData.append("comment", " ");
424
- const response = await fetch(url.toString(), {
425
- method: "POST",
426
- body: formData,
426
+
427
+ const parameterObject: Record<string, any> = {};
428
+ if (data.parameters && data.parameters.length > 0) {
429
+ data.parameters.forEach((param) => {
430
+ const key = param.configKey || param.name;
431
+ const paramType = param.type?.toLowerCase();
432
+ if (paramType === "number" || paramType === "integer") {
433
+ parameterObject[key] = Number(param.value) || 0;
434
+ } else if (paramType === "boolean") {
435
+ parameterObject[key] = param.value === "true";
436
+ } else {
437
+ parameterObject[key] = param.value;
438
+ }
427
439
  });
428
- if (!response.ok) {
429
- throw new Error(`HTTP error! status: ${response.status}`);
430
- }
440
+ }
431
441
 
432
- const jsonResponse = await response.json();
433
- const isTimeout = jsonResponse?.events?.some(
434
- (event: any) => event.content === "_timeout_",
435
- );
442
+ const resourceObject: Record<string, any> = {};
443
+ if (data.resources && data.resources.length > 0) {
444
+ data.resources.forEach((resource) => {
445
+ if (resource.type === "volume") {
446
+ resourceObject[resource.name] = {
447
+ volume: {
448
+ kind: "storage",
449
+ size: parseInt(resource.value) || 1,
450
+ unit: "G",
451
+ type: resource.kind,
452
+ },
453
+ };
454
+ } else if (resource.type === "secret") {
455
+ resourceObject[resource.name] = {
456
+ secret: `${data.tenant}/${resource.value}`,
457
+ };
458
+ } else if (resource.type === "domain") {
459
+ resourceObject[resource.name] = {
460
+ domain: `${data.tenant}/${resource.value}`,
461
+ };
462
+ } else if (resource.type === "ca") {
463
+ resourceObject[resource.name] = {
464
+ ca: `${data.tenant}/${resource.value}`,
465
+ };
466
+ } else if (resource.type === "certificate") {
467
+ resourceObject[resource.name] = {
468
+ certificate: `${data.tenant}/${resource.value}`,
469
+ };
470
+ } else if (resource.type === "port") {
471
+ resourceObject[resource.name] = {
472
+ port: `${data.tenant}/${resource.value}`,
473
+ };
474
+ }
475
+ });
476
+ }
436
477
 
437
- if (isTimeout) {
438
- console.error("Timeout en la petición:", {
439
- isOk: false,
440
- code: "TIMEOUT",
441
- error: "_timeout_",
442
- });
478
+ let previousRevision = 1;
479
+ if (data.currentRevision) {
480
+ previousRevision = parseInt(data.currentRevision.toString(), 10);
481
+ if (isNaN(previousRevision)) {
482
+ console.warn("currentRevision is not a valid number, using 1");
483
+ previousRevision = 1;
443
484
  }
444
485
  } else {
445
- const parameterObject: Record<string, any> = {};
446
- if (data.parameters && data.parameters.length > 0) {
447
- data.parameters.forEach((param) => {
448
- const key = param.configKey || param.name;
449
- const paramType = param.type?.toLowerCase();
450
- if (paramType === "number" || paramType === "integer") {
451
- parameterObject[key] = Number(param.value) || 0;
452
- } else if (paramType === "boolean") {
453
- parameterObject[key] = param.value === "true";
454
- } else {
455
- parameterObject[key] = param.value;
456
- }
457
- });
458
- }
459
-
460
- const resourceObject: Record<string, any> = {};
461
- if (data.resources && data.resources.length > 0) {
462
- data.resources.forEach((resource) => {
463
- if (resource.type === "volume") {
464
- resourceObject[resource.name] = {
465
- volume: {
466
- kind: "storage",
467
- size: parseInt(resource.value) || 1,
468
- unit: "G",
469
- type: resource.kind,
470
- },
471
- };
472
- } else if (resource.type === "secret") {
473
- resourceObject[resource.name] = {
474
- secret: `${data.tenant}/${resource.value}`,
475
- };
476
- } else if (resource.type === "domain") {
477
- resourceObject[resource.name] = {
478
- domain: `${data.tenant}/${resource.value}`,
479
- };
480
- } else if (resource.type === "ca") {
481
- resourceObject[resource.name] = {
482
- ca: `${data.tenant}/${resource.value}`,
483
- };
484
- } else if (resource.type === "certificate") {
485
- resourceObject[resource.name] = {
486
- certificate: `${data.tenant}/${resource.value}`,
487
- };
488
- } else if (resource.type === "port") {
489
- resourceObject[resource.name] = {
490
- port: `${data.tenant}/${resource.value}`,
491
- };
492
- }
493
- });
494
- }
486
+ previousRevision = getLatestRevision(data.revisions) || 1;
487
+ }
495
488
 
496
- let previousRevision = 1;
497
- if (data.currentRevision) {
498
- previousRevision = parseInt(data.currentRevision.toString(), 10);
499
- if (isNaN(previousRevision)) {
500
- console.warn("currentRevision is not a valid number, using 1");
501
- previousRevision = 1;
502
- }
503
- } else {
504
- previousRevision = getLatestRevision(data.revisions) || 1;
505
- }
489
+ const scaleConfig: any = {};
490
+ if (data.role && data.role.length > 0) {
491
+ scaleConfig.detail = {};
492
+ data.role.forEach((role) => {
493
+ scaleConfig.detail[role.name] = {
494
+ hsize: role.hsize || scale || data.minReplicas || 1,
495
+ };
496
+ });
497
+ } else {
498
+ scaleConfig.hsize = scale || data.minReplicas || 1;
499
+ }
500
+ const meta = {
501
+ scaling: {
502
+ simple:
503
+ data.role?.reduce(
504
+ (acc, role) => {
505
+ if (role.scalling && role.name) {
506
+ acc[role.name] = {
507
+ scale_up: {
508
+ cpu: Math.min(parseInt(role.scalling.cpu.up) || 0, 100),
509
+ memory: Math.min(
510
+ parseInt(role.scalling.memory.up) || 0,
511
+ 100,
512
+ ),
513
+ },
514
+ scale_down: {
515
+ cpu: Math.min(parseInt(role.scalling.cpu.down) || 0, 100),
516
+ memory: Math.min(
517
+ parseInt(role.scalling.memory.down) || 0,
518
+ 100,
519
+ ),
520
+ },
521
+ hysteresis: parseInt(role.scalling.histeresys) || 0,
522
+ min_replicas: role.scalling.instances.min || 0,
523
+ max_replicas: role.scalling.instances.max || 0,
524
+ };
525
+ }
526
+ return acc;
527
+ },
528
+ {} as Record<string, any>,
529
+ ) || {},
530
+ },
531
+ };
506
532
 
507
- const scaleConfig: any = {};
508
- if (data.role && data.role.length > 0) {
509
- scaleConfig.detail = {};
510
- data.role.forEach((role) => {
511
- scaleConfig.detail[role.name] = {
512
- hsize: role.hsize || scale || data.minReplicas || 1,
513
- };
514
- });
515
- } else {
516
- scaleConfig.hsize = scale || data.minReplicas || 1;
517
- }
518
- const meta = {
519
- scaling: {
520
- simple:
521
- data.role?.reduce(
522
- (acc, role) => {
523
- if (role.scalling && role.name) {
524
- acc[role.name] = {
525
- scale_up: {
526
- cpu: Math.min(parseInt(role.scalling.cpu.up) || 0, 100),
527
- memory: Math.min(
528
- parseInt(role.scalling.memory.up) || 0,
529
- 100,
530
- ),
531
- },
532
- scale_down: {
533
- cpu: Math.min(parseInt(role.scalling.cpu.down) || 0, 100),
534
- memory: Math.min(
535
- parseInt(role.scalling.memory.down) || 0,
536
- 100,
537
- ),
538
- },
539
- hysteresis: parseInt(role.scalling.histeresys) || 0,
540
- min_replicas: role.scalling.instances.min || 0,
541
- max_replicas: role.scalling.instances.max || 0,
542
- };
543
- }
544
- return acc;
545
- },
546
- {} as Record<string, any>,
547
- ) || {},
533
+ const updateBody = {
534
+ spec: {
535
+ type: "update-config",
536
+ comment: "Service configuration update",
537
+ config: {
538
+ parameter: parameterObject,
539
+ resource: resourceObject,
540
+ resilience: 0,
541
+ scale: scaleConfig,
548
542
  },
549
- };
543
+ meta: meta,
544
+ },
545
+ tenant: data.tenant,
546
+ service: data.name,
547
+ previous: previousRevision,
548
+ };
550
549
 
551
- const updateBody = {
552
- spec: {
553
- type: "update-config",
554
- comment: "Service configuration update",
555
- config: {
556
- parameter: parameterObject,
557
- resource: resourceObject,
558
- resilience: 0,
559
- scale: scaleConfig,
560
- },
561
- meta: meta,
562
- },
563
- tenant: data.tenant,
564
- service: data.name,
565
- previous: previousRevision,
566
- };
550
+ const response = await makeGlobalWebSocketRequest(
551
+ "revision:update_revision",
552
+ updateBody,
553
+ 30000,
554
+ "UPDATE_CONFIG",
555
+ data.name,
556
+ );
567
557
 
568
- const response = await makeGlobalWebSocketRequest(
569
- "revision:update_revision",
570
- updateBody,
571
- 30000,
572
- "UPDATE_CONFIG",
573
- data.name,
574
- );
575
- const updatedService: Service = {
576
- ...data,
577
- status: {
578
- code: "PENDING",
579
- message: "",
580
- timestamp: "",
581
- args: [],
582
- },
583
- };
584
- eventHelper.service.publish.updated(updatedService);
585
-
586
- const updateNotification: Notification = {
587
- type: "success",
588
- subtype: "service-updated",
589
- date: Date.now().toString(),
590
- status: "unread",
591
- callToAction: false,
592
- data: {
593
- service: data.name,
594
- tenant: data.tenant,
595
- },
596
- };
597
-
598
- eventHelper.notification.publish.creation(updateNotification);
599
- return response;
600
- }
558
+ const updatedService: Service = {
559
+ ...data,
560
+ status: {
561
+ code: "PENDING",
562
+ message: "",
563
+ timestamp: "",
564
+ args: [],
565
+ },
566
+ };
567
+ eventHelper.service.publish.updated(updatedService);
601
568
 
569
+ const updateNotification: Notification = {
570
+ type: "success",
571
+ subtype: "service-updated",
572
+ date: Date.now().toString(),
573
+ status: "unread",
574
+ callToAction: false,
575
+ data: {
576
+ service: data.name,
577
+ tenant: data.tenant,
578
+ },
579
+ };
580
+
581
+ eventHelper.notification.publish.creation(updateNotification);
582
+ return response;
602
583
  } catch (err) {
603
584
  console.error("Error updating service configuration via WebSocket:", err);
604
585
  const notification: Notification = {
@@ -623,7 +604,7 @@ export const updateService = async (
623
604
  };
624
605
  export const unlinkServices = async (service: Service, token: string) => {
625
606
  const deleteLinks: Link[] = service.links.filter(
626
- (link) => link.delete === true
607
+ (link) => link.delete === true,
627
608
  );
628
609
  if (deleteLinks.length > 0) {
629
610
  await Promise.all(
@@ -638,7 +619,7 @@ export const unlinkServices = async (service: Service, token: string) => {
638
619
  unlinkBody,
639
620
  30000,
640
621
  "UNLINK",
641
- service.name
622
+ service.name,
642
623
  );
643
624
 
644
625
  const unlinkNotification: Notification = {
@@ -674,7 +655,7 @@ export const unlinkServices = async (service: Service, token: string) => {
674
655
  };
675
656
  eventHelper.notification.publish.creation(notification);
676
657
  }
677
- })
658
+ }),
678
659
  );
679
660
  }
680
661
  };
@@ -699,7 +680,7 @@ export const updateServiceLinks = async (link: Link, token: string) => {
699
680
  linkBody,
700
681
  30000,
701
682
  "UNLINK",
702
- link.origin
683
+ link.origin,
703
684
  );
704
685
 
705
686
  // const unlinkNotification: Notification = {
@@ -745,7 +726,7 @@ export const updateServiceLinks = async (link: Link, token: string) => {
745
726
  linkBody,
746
727
  30000,
747
728
  "LINK",
748
- link.origin
729
+ link.origin,
749
730
  );
750
731
 
751
732
  const notification: Notification = {
@@ -801,7 +782,7 @@ export const changeRevision = async (data: Service, token: string) => {
801
782
  revisionBody,
802
783
  30000,
803
784
  "UPDATE_REVISION",
804
- data.name
785
+ data.name,
805
786
  );
806
787
  const notification: Notification = {
807
788
  type: "success",
@@ -833,10 +814,10 @@ export const changeRevision = async (data: Service, token: string) => {
833
814
  eventHelper.notification.publish.creation(notification);
834
815
  }
835
816
  };
836
- function getLatestRevision(revisions: Revision[]): number | null {
817
+ function getLatestRevision(revisions: string[]): number | null {
837
818
  if (!revisions || revisions.length === 0) {
838
819
  return null;
839
820
  }
840
-
841
- return Math.max(...revisions.map((revision) => Number(revision.id)));
842
- }
821
+
822
+ return Math.max(...revisions.map(Number));
823
+ }