mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
committed by
Aiden McClelland
parent
5bcad69cf7
commit
7213d82f1b
@@ -11,7 +11,6 @@ const ICONS = [
|
||||
'arrow-back',
|
||||
'arrow-forward',
|
||||
'arrow-up',
|
||||
'briefcase-outline',
|
||||
'brush-outline',
|
||||
'bookmark-outline',
|
||||
'cellular-outline',
|
||||
|
||||
@@ -2,18 +2,19 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppPropertiesPage } from './app-properties.page'
|
||||
import { AppCredentialsPage } from './app-credentials.page'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { MaskPipeModule } from 'src/app/pipes/mask/mask.module'
|
||||
import {
|
||||
SharedPipesModule,
|
||||
TextSpinnerComponentModule,
|
||||
} from '@start9labs/shared'
|
||||
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppPropertiesPage,
|
||||
component: AppCredentialsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -26,7 +27,8 @@ const routes: Routes = [
|
||||
SharedPipesModule,
|
||||
TextSpinnerComponentModule,
|
||||
MaskPipeModule,
|
||||
SkeletonListComponentModule,
|
||||
],
|
||||
declarations: [AppPropertiesPage],
|
||||
declarations: [AppCredentialsPage],
|
||||
})
|
||||
export class AppPropertiesPageModule {}
|
||||
export class AppCredentialsPageModule {}
|
||||
@@ -0,0 +1,58 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Credentials</ion-title>
|
||||
<ion-buttons *ngIf="!loading" slot="end">
|
||||
<ion-button (click)="refresh()">
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
Refresh
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<!-- loading -->
|
||||
<skeleton-list *ngIf="loading; else loaded"></skeleton-list>
|
||||
|
||||
<!-- loaded -->
|
||||
<ng-template #loaded>
|
||||
<!-- no credentials -->
|
||||
<ion-item *ngIf="credentials | empty else hasCredentials">
|
||||
<ion-label>
|
||||
<p>No credentials</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-template #hasCredentials>
|
||||
<ion-item-group>
|
||||
<ion-item *ngFor="let cred of credentials | keyvalue: asIsOrder">
|
||||
<ion-label>
|
||||
<h2>{{ cred.key }}</h2>
|
||||
<p class="courier-new">
|
||||
{{ unmasked[cred.key] ? cred.value : (cred.value | mask : 64) }}
|
||||
</p>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<ion-button fill="clear" (click)="toggleMask(cred.key)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
[name]="unmasked[cred.key] ? 'eye-off-outline' : 'eye-outline'"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copy(cred.value)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
name="copy-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import {
|
||||
ErrorToastService,
|
||||
getPkgId,
|
||||
copyToClipboard,
|
||||
pauseFor,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'app-credentials',
|
||||
templateUrl: './app-credentials.page.html',
|
||||
styleUrls: ['./app-credentials.page.scss'],
|
||||
})
|
||||
export class AppCredentialsPage {
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
credentials: Record<string, string> = {}
|
||||
unmasked: { [key: string]: boolean } = {}
|
||||
loading = true
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.getCredentials()
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.getCredentials()
|
||||
}
|
||||
|
||||
async copy(text: string): Promise<void> {
|
||||
const success = await copyToClipboard(text)
|
||||
const message = success
|
||||
? 'Copied. Clearing clipboard in 20 seconds'
|
||||
: 'Failed to copy.'
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 2000,
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
toggleMask(key: string) {
|
||||
this.unmasked[key] = !this.unmasked[key]
|
||||
}
|
||||
|
||||
private async getCredentials(): Promise<void> {
|
||||
this.loading = true
|
||||
try {
|
||||
this.credentials = await this.embassyApi.getPackageCredentials({
|
||||
id: this.pkgId,
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Properties</ion-title>
|
||||
<ion-buttons *ngIf="!loading" slot="end">
|
||||
<ion-button (click)="refresh()">
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
Refresh
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<text-spinner
|
||||
*ngIf="loading; else loaded"
|
||||
text="Loading Properties"
|
||||
></text-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
<!-- not running -->
|
||||
<ion-item *ngIf="stopped$ | async" class="ion-margin-bottom">
|
||||
<ion-label>
|
||||
<p>
|
||||
<ion-text color="warning">
|
||||
Service is stopped. Information on this page could be inaccurate.
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- no properties -->
|
||||
<ion-item *ngIf="properties | empty">
|
||||
<ion-label>
|
||||
<p>No properties.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- properties -->
|
||||
<ion-item-group *ngIf="!(properties | empty)">
|
||||
<div *ngFor="let prop of node | keyvalue: asIsOrder">
|
||||
<!-- object -->
|
||||
<ion-item
|
||||
button
|
||||
detail="true"
|
||||
*ngIf="prop.value.type === 'object'"
|
||||
(click)="goToNested(prop.key)"
|
||||
>
|
||||
<ion-button
|
||||
*ngIf="prop.value.description"
|
||||
fill="clear"
|
||||
slot="start"
|
||||
(click)="presentDescription(prop, $event)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label>
|
||||
<h2>{{ prop.key }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- not object -->
|
||||
<ion-item *ngIf="prop.value.type === 'string'">
|
||||
<ion-button
|
||||
*ngIf="prop.value.description"
|
||||
fill="clear"
|
||||
slot="start"
|
||||
(click)="presentDescription(prop, $event)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label>
|
||||
<h2>{{ prop.key }}</h2>
|
||||
<p class="courier-new">
|
||||
{{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value |
|
||||
mask : 64) : prop.value.value }}
|
||||
</p>
|
||||
</ion-label>
|
||||
<div slot="end" *ngIf="prop.value.copyable || prop.value.qr">
|
||||
<ion-button
|
||||
*ngIf="prop.value.masked"
|
||||
fill="clear"
|
||||
(click)="toggleMask(prop.key)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
[name]="unmasked[prop.key] ? 'eye-off-outline' : 'eye-outline'"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="prop.value.qr"
|
||||
fill="clear"
|
||||
(click)="showQR(prop.value.value)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
name="qr-code-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="prop.value.copyable"
|
||||
fill="clear"
|
||||
(click)="copy(prop.value.value)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
name="copy-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -1,154 +0,0 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
AlertController,
|
||||
IonBackButtonDelegate,
|
||||
ModalController,
|
||||
NavController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import { PackageProperties } from 'src/app/util/properties.util'
|
||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
PackageMainStatus,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
ErrorToastService,
|
||||
getPkgId,
|
||||
copyToClipboard,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiDestroyService } from '@taiga-ui/cdk'
|
||||
import { getValueByPointer } from 'fast-json-patch'
|
||||
import { map, takeUntil } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'app-properties',
|
||||
templateUrl: './app-properties.page.html',
|
||||
styleUrls: ['./app-properties.page.scss'],
|
||||
providers: [TuiDestroyService],
|
||||
})
|
||||
export class AppPropertiesPage {
|
||||
loading = true
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
|
||||
pointer = ''
|
||||
node: PackageProperties = {}
|
||||
|
||||
properties: PackageProperties = {}
|
||||
unmasked: { [key: string]: boolean } = {}
|
||||
|
||||
stopped$ = this.patch
|
||||
.watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status')
|
||||
.pipe(map(status => status === PackageMainStatus.Stopped))
|
||||
|
||||
@ViewChild(IonBackButtonDelegate, { static: false })
|
||||
backButton?: IonBackButtonDelegate
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly destroy$: TuiDestroyService,
|
||||
) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
if (!this.backButton) return
|
||||
this.backButton.onClick = () => {
|
||||
history.back()
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.getProperties()
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(queryParams => {
|
||||
if (queryParams['pointer'] === this.pointer) return
|
||||
this.pointer = queryParams['pointer'] || ''
|
||||
this.node = getValueByPointer(this.properties, this.pointer)
|
||||
})
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.getProperties()
|
||||
}
|
||||
|
||||
async presentDescription(
|
||||
property: { key: string; value: PackageProperties[''] },
|
||||
e: Event,
|
||||
) {
|
||||
e.stopPropagation()
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: property.key,
|
||||
message: property.value.description || undefined,
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async goToNested(key: string): Promise<any> {
|
||||
this.navCtrl.navigateForward(`/services/${this.pkgId}/properties`, {
|
||||
queryParams: {
|
||||
pointer: `${this.pointer}/${key}/value`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async copy(text: string): Promise<void> {
|
||||
let message = ''
|
||||
await copyToClipboard(text).then(success => {
|
||||
message = success
|
||||
? 'Copied to clipboard!'
|
||||
: 'Failed to copy to clipboard.'
|
||||
})
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async showQR(text: string): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: QRComponent,
|
||||
componentProps: {
|
||||
text,
|
||||
},
|
||||
cssClass: 'qr-modal',
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
toggleMask(key: string) {
|
||||
this.unmasked[key] = !this.unmasked[key]
|
||||
}
|
||||
|
||||
private async getProperties(): Promise<void> {
|
||||
this.loading = true
|
||||
try {
|
||||
this.properties = await this.embassyApi.getPackageProperties({
|
||||
id: this.pkgId,
|
||||
})
|
||||
this.node = getValueByPointer(this.properties, this.pointer)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -62,16 +62,15 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
description: `Customize ${pkgTitle}`,
|
||||
icon: 'options-outline',
|
||||
},
|
||||
// properties
|
||||
// credentials
|
||||
{
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['properties'], {
|
||||
this.navCtrl.navigateForward(['credentials'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
title: 'Properties',
|
||||
description:
|
||||
'Runtime information, credentials, and other values of interest',
|
||||
icon: 'briefcase-outline',
|
||||
title: 'Credentials',
|
||||
description: 'Password, keys, or other credentials of interest',
|
||||
icon: 'key-outline',
|
||||
},
|
||||
// actions
|
||||
{
|
||||
|
||||
@@ -37,10 +37,10 @@ const routes: Routes = [
|
||||
import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/properties',
|
||||
path: ':pkgId/credentials',
|
||||
loadChildren: () =>
|
||||
import('./app-properties/app-properties.module').then(
|
||||
m => m.AppPropertiesPageModule,
|
||||
import('./app-credentials/app-credentials.module').then(
|
||||
m => m.AppCredentialsPageModule,
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Dump, Revision } from 'patch-db-client'
|
||||
import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace'
|
||||
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
import {
|
||||
DataModel,
|
||||
@@ -235,9 +234,8 @@ export module RR {
|
||||
|
||||
// package
|
||||
|
||||
export type GetPackagePropertiesReq = { id: string } // package.properties
|
||||
export type GetPackagePropertiesRes<T extends number> =
|
||||
PackagePropertiesVersioned<T>
|
||||
export type GetPackageCredentialsReq = { id: string } // package.credentials
|
||||
export type GetPackageCredentialsRes = Record<string, string>
|
||||
|
||||
export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs
|
||||
export type GetPackageLogsRes = LogsRes
|
||||
|
||||
@@ -234,9 +234,9 @@ export abstract class ApiService {
|
||||
|
||||
// package
|
||||
|
||||
abstract getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes<2>['data']>
|
||||
abstract getPackageCredentials(
|
||||
params: RR.GetPackageCredentialsReq,
|
||||
): Promise<RR.GetPackageCredentialsRes>
|
||||
|
||||
abstract getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { Observable } from 'rxjs'
|
||||
@@ -405,12 +404,10 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
|
||||
return this.rpcRequest({ method: 'package.properties', params }).then(
|
||||
parsePropertiesPermissive,
|
||||
)
|
||||
async getPackageCredentials(
|
||||
params: RR.GetPackageCredentialsReq,
|
||||
): Promise<RR.GetPackageCredentialsRes> {
|
||||
return this.rpcRequest({ method: 'package.credentials', params })
|
||||
}
|
||||
|
||||
async getPackageLogs(
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { Mock } from './api.fixures'
|
||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||
import {
|
||||
@@ -667,12 +666,13 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
|
||||
async getPackageCredentials(
|
||||
params: RR.GetPackageCredentialsReq,
|
||||
): Promise<RR.GetPackageCredentialsRes> {
|
||||
await pauseFor(2000)
|
||||
return '' as any
|
||||
// return parsePropertiesPermissive(Mock.PackageProperties)
|
||||
return {
|
||||
password: 'specialPassword$',
|
||||
}
|
||||
}
|
||||
|
||||
async getPackageLogs(
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import { applyOperation } from 'fast-json-patch'
|
||||
import matches, {
|
||||
Parser,
|
||||
shape,
|
||||
string,
|
||||
literal,
|
||||
boolean,
|
||||
deferred,
|
||||
dictionary,
|
||||
anyOf,
|
||||
number,
|
||||
arrayOf,
|
||||
} from 'ts-matches'
|
||||
|
||||
type ValidVersion = 1 | 2
|
||||
|
||||
type PropertiesV1 = typeof matchPropertiesV1._TYPE
|
||||
type PackagePropertiesV1 = PropertiesV1[]
|
||||
type PackagePropertiesV2 = {
|
||||
[name: string]: PackagePropertyString | PackagePropertyObject
|
||||
}
|
||||
type PackagePropertiesVersionedData<T extends number> = T extends 1
|
||||
? PackagePropertiesV1
|
||||
: T extends 2
|
||||
? PackagePropertiesV2
|
||||
: never
|
||||
|
||||
type PackagePropertyString = typeof matchPackagePropertyString._TYPE
|
||||
|
||||
export type PackagePropertiesVersioned<T extends number> = {
|
||||
version: T
|
||||
data: PackagePropertiesVersionedData<T>
|
||||
}
|
||||
export type PackageProperties = PackagePropertiesV2
|
||||
|
||||
const matchPropertiesV1 = shape(
|
||||
{
|
||||
name: string,
|
||||
value: string,
|
||||
description: string,
|
||||
copyable: boolean,
|
||||
qr: boolean,
|
||||
},
|
||||
['description', 'copyable', 'qr'],
|
||||
{ copyable: false, qr: false } as const,
|
||||
)
|
||||
|
||||
const [matchPackagePropertiesV2, setPPV2] = deferred<PackagePropertiesV2>()
|
||||
const matchPackagePropertyString = shape(
|
||||
{
|
||||
type: literal('string'),
|
||||
description: string,
|
||||
value: string,
|
||||
copyable: boolean,
|
||||
qr: boolean,
|
||||
masked: boolean,
|
||||
},
|
||||
['description', 'copyable', 'qr', 'masked'],
|
||||
{
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: false,
|
||||
} as const,
|
||||
)
|
||||
const matchPackagePropertyObject = shape(
|
||||
{
|
||||
type: literal('object'),
|
||||
value: matchPackagePropertiesV2,
|
||||
description: string,
|
||||
},
|
||||
['description'],
|
||||
)
|
||||
|
||||
const matchPropertyV2 = anyOf(
|
||||
matchPackagePropertyString,
|
||||
matchPackagePropertyObject,
|
||||
)
|
||||
type PackagePropertyObject = typeof matchPackagePropertyObject._TYPE
|
||||
setPPV2(dictionary([string, matchPropertyV2]))
|
||||
|
||||
const matchPackagePropertiesVersionedV1 = shape({
|
||||
version: number,
|
||||
data: arrayOf(matchPropertiesV1),
|
||||
})
|
||||
const matchPackagePropertiesVersionedV2 = shape({
|
||||
version: number,
|
||||
data: dictionary([string, matchPropertyV2]),
|
||||
})
|
||||
|
||||
export function parsePropertiesPermissive(
|
||||
properties: unknown,
|
||||
errorCallback: (err: Error) => any = console.warn,
|
||||
): PackageProperties {
|
||||
return matches(properties)
|
||||
.when(matchPackagePropertiesVersionedV1, prop =>
|
||||
parsePropertiesV1Permissive(prop.data, errorCallback),
|
||||
)
|
||||
.when(matchPackagePropertiesVersionedV2, prop => prop.data)
|
||||
.when(matches.nill, {})
|
||||
.defaultToLazy(() => {
|
||||
errorCallback(new TypeError(`value is not valid`))
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
function parsePropertiesV1Permissive(
|
||||
properties: unknown,
|
||||
errorCallback: (err: Error) => any,
|
||||
): PackageProperties {
|
||||
if (!Array.isArray(properties)) {
|
||||
errorCallback(new TypeError(`${properties} is not an array`))
|
||||
return {}
|
||||
}
|
||||
return properties.reduce(
|
||||
(prev: PackagePropertiesV2, cur: unknown, idx: number) => {
|
||||
const result = matchPropertiesV1.enumParsed(cur)
|
||||
if ('value' in result) {
|
||||
const value = result.value
|
||||
prev[value.name] = {
|
||||
type: 'string',
|
||||
value: value.value,
|
||||
description: value.description,
|
||||
copyable: value.copyable,
|
||||
qr: value.qr,
|
||||
masked: false,
|
||||
}
|
||||
} else {
|
||||
const error = result.error
|
||||
const message = Parser.validatorErrorAsString(error)
|
||||
const dataPath = error.keys.map(removeQuotes).join('/')
|
||||
errorCallback(new Error(`/data/${idx}: ${message}`))
|
||||
if (dataPath) {
|
||||
applyOperation(cur, {
|
||||
op: 'replace',
|
||||
path: `/${dataPath}`,
|
||||
value: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
return prev
|
||||
},
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
const removeRegex = /('|")/
|
||||
function removeQuotes(x: string) {
|
||||
while (removeRegex.test(x)) {
|
||||
x = x.replace(removeRegex, '')
|
||||
}
|
||||
return x
|
||||
}
|
||||
Reference in New Issue
Block a user