don't be so fragile when comparing marketplace URLs (#2040)

* don't be so fragile when comparing marketplace URLs

* handle more edges

* minor

* clean up a little
This commit is contained in:
Matt Hill
2022-12-15 12:00:01 -07:00
committed by GitHub
parent 92cd85b204
commit fd7abdb8a4
7 changed files with 49 additions and 33 deletions

View File

@@ -10,14 +10,6 @@ export function pauseFor(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)) return new Promise(resolve => setTimeout(resolve, ms))
} }
export function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1)
}
export function exists<T>(t: T | undefined): t is T {
return t !== undefined
}
export function debounce(delay: number = 300): MethodDecorator { export function debounce(delay: number = 300): MethodDecorator {
return function ( return function (
target: any, target: any,
@@ -37,13 +29,16 @@ export function debounce(delay: number = 300): MethodDecorator {
} }
} }
export function removeTrailingSlash(word: string): string { export function sameUrl(
return word.replace(/\/+$/, '') u1: string | null | undefined,
u2: string | null | undefined,
): boolean {
return toUrl(u1) === toUrl(u2)
} }
export function isValidHttpUrl(string: string): boolean { export function isValidHttpUrl(url: string): boolean {
try { try {
const _ = new URL(string) const _ = new URL(url)
return true return true
} catch (_) { } catch (_) {
return false return false
@@ -53,3 +48,12 @@ export function isValidHttpUrl(string: string): boolean {
export function getUrlHostname(url: string): string { export function getUrlHostname(url: string): string {
return new URL(url).hostname return new URL(url).hostname
} }
export function toUrl(text: string | null | undefined): string {
try {
const url = new URL(text as string)
return url.toString()
} catch {
return ''
}
}

View File

@@ -6,6 +6,7 @@ import {
PipeTransform, PipeTransform,
} from '@angular/core' } from '@angular/core'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { sameUrl } from '@start9labs/shared'
@Component({ @Component({
selector: 'store-icon', selector: 'store-icon',
@@ -26,9 +27,9 @@ export class GetIconPipe implements PipeTransform {
transform(url: string): string | null { transform(url: string): string | null {
const { start9, community } = this.config.marketplace const { start9, community } = this.config.marketplace
if (url === start9) { if (sameUrl(url, start9)) {
return 'assets/img/icon.png' return 'assets/img/icon.png'
} else if (url === community) { } else if (sameUrl(url, community)) {
return 'assets/img/community-store.png' return 'assets/img/community-store.png'
} }
return null return null

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { endWith, Observable } from 'rxjs' import { endWith, Observable } from 'rxjs'
import { filter, map, pairwise } from 'rxjs/operators' import { map, pairwise } from 'rxjs/operators'
import { exists } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -10,7 +9,6 @@ export class NotificationsToastService extends Observable<boolean> {
private readonly stream$ = this.patch private readonly stream$ = this.patch
.watch$('server-info', 'unread-notification-count') .watch$('server-info', 'unread-notification-count')
.pipe( .pipe(
filter(exists),
pairwise(), pairwise(),
map(([prev, cur]) => cur > prev), map(([prev, cur]) => cur > prev),
endWith(false), endWith(false),

View File

@@ -6,7 +6,12 @@ import {
ModalController, ModalController,
} from '@ionic/angular' } from '@ionic/angular'
import { ActionSheetButton } from '@ionic/core' import { ActionSheetButton } from '@ionic/core'
import { ErrorToastService } from '@start9labs/shared' import {
ErrorToastService,
isValidHttpUrl,
sameUrl,
toUrl,
} from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace' import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ValueSpecObject } from 'src/app/pkg-config/config-types' import { ValueSpecObject } from 'src/app/pkg-config/config-types'
@@ -31,7 +36,7 @@ export class MarketplaceSettingsPage {
map(([stores, selected]) => { map(([stores, selected]) => {
const toSlice = stores.map(s => ({ const toSlice = stores.map(s => ({
...s, ...s,
selected: s.url === selected.url, selected: sameUrl(s.url, selected.url),
})) }))
// 0 and 1 are prod and community // 0 and 1 are prod and community
const standard = toSlice.slice(0, 1) const standard = toSlice.slice(0, 1)
@@ -69,13 +74,13 @@ export class MarketplaceSettingsPage {
{ {
text: 'Save for Later', text: 'Save for Later',
handler: (value: { url: string }) => { handler: (value: { url: string }) => {
this.saveOnly(new URL(value.url)) this.saveOnly(value.url)
}, },
}, },
{ {
text: 'Save and Connect', text: 'Save and Connect',
handler: (value: { url: string }) => { handler: (value: { url: string }) => {
this.saveAndConnect(new URL(value.url)) this.saveAndConnect(value.url)
}, },
isSubmit: true, isSubmit: true,
}, },
@@ -160,10 +165,11 @@ export class MarketplaceSettingsPage {
} }
} }
private async saveOnly(url: URL): Promise<void> { private async saveOnly(rawUrl: string): Promise<void> {
const loader = await this.loadingCtrl.create() const loader = await this.loadingCtrl.create()
try { try {
const url = new URL(rawUrl).toString()
await this.validateAndSave(url, loader) await this.validateAndSave(url, loader)
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
@@ -172,12 +178,13 @@ export class MarketplaceSettingsPage {
} }
} }
private async saveAndConnect(url: URL): Promise<void> { private async saveAndConnect(rawUrl: string): Promise<void> {
const loader = await this.loadingCtrl.create() const loader = await this.loadingCtrl.create()
try { try {
const url = new URL(rawUrl).toString()
await this.validateAndSave(url, loader) await this.validateAndSave(url, loader)
await this.connect(url.toString(), loader) await this.connect(url, loader)
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -186,15 +193,14 @@ export class MarketplaceSettingsPage {
} }
private async validateAndSave( private async validateAndSave(
urlObj: URL, url: string,
loader: HTMLIonLoadingElement, loader: HTMLIonLoadingElement,
): Promise<void> { ): Promise<void> {
const url = urlObj.toString()
// Error on duplicates // Error on duplicates
const hosts = await firstValueFrom( const hosts = await firstValueFrom(
this.patch.watch$('ui', 'marketplace', 'known-hosts'), this.patch.watch$('ui', 'marketplace', 'known-hosts'),
) )
const currentUrls = Object.keys(hosts) const currentUrls = Object.keys(hosts).map(toUrl)
if (currentUrls.includes(url)) throw new Error('marketplace already added') if (currentUrls.includes(url)) throw new Error('marketplace already added')
// Validate // Validate
@@ -225,7 +231,7 @@ export class MarketplaceSettingsPage {
) )
const filtered: { [url: string]: UIStore } = Object.keys(hosts) const filtered: { [url: string]: UIStore } = Object.keys(hosts)
.filter(key => key !== url) .filter(key => !sameUrl(key, url))
.reduce((prev, curr) => { .reduce((prev, curr) => {
const name = hosts[curr] const name = hosts[curr]
return { return {

View File

@@ -9,7 +9,12 @@ import {
AbstractMarketplaceService, AbstractMarketplaceService,
MarketplacePkg, MarketplacePkg,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { Emver, ErrorToastService, isEmptyObject } from '@start9labs/shared' import {
Emver,
ErrorToastService,
isEmptyObject,
sameUrl,
} from '@start9labs/shared'
import { import {
DataModel, DataModel,
PackageDataEntry, PackageDataEntry,
@@ -71,7 +76,7 @@ export class MarketplaceShowControlsComponent {
} else { } else {
const originalUrl = this.localPkg.installed?.['marketplace-url'] const originalUrl = this.localPkg.installed?.['marketplace-url']
if (url !== originalUrl) { if (!sameUrl(url, originalUrl)) {
const proceed = await this.presentAlertDifferentMarketplace( const proceed = await this.presentAlertDifferentMarketplace(
url, url,
originalUrl, originalUrl,

View File

@@ -14,7 +14,7 @@ import {
MarketplacePkg, MarketplacePkg,
StoreIdentity, StoreIdentity,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { Emver, isEmptyObject } from '@start9labs/shared' import { Emver, isEmptyObject, sameUrl } from '@start9labs/shared'
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { combineLatest, Observable } from 'rxjs' import { combineLatest, Observable } from 'rxjs'
import { PrimaryRendering } from '../../services/pkg-status-rendering.service' import { PrimaryRendering } from '../../services/pkg-status-rendering.service'
@@ -194,7 +194,8 @@ export function marketplaceSame(
local: Record<string, PackageDataEntry>, local: Record<string, PackageDataEntry>,
url: string, url: string,
): boolean { ): boolean {
return local[id]?.installed?.['marketplace-url'] === url const localUrl = local[id]?.installed?.['marketplace-url']
return sameUrl(localUrl, url)
} }
export function versionLower( export function versionLower(

View File

@@ -33,6 +33,7 @@ import {
tap, tap,
} from 'rxjs/operators' } from 'rxjs/operators'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { sameUrl } from '@start9labs/shared'
@Injectable() @Injectable()
export class MarketplaceService implements AbstractMarketplaceService { export class MarketplaceService implements AbstractMarketplaceService {
@@ -68,7 +69,7 @@ export class MarketplaceService implements AbstractMarketplaceService {
startWith<StoreIdentity[]>([]), startWith<StoreIdentity[]>([]),
pairwise(), pairwise(),
mergeMap(([prev, curr]) => mergeMap(([prev, curr]) =>
curr.filter(c => !prev.find(p => c.url === p.url)), curr.filter(c => !prev.find(p => sameUrl(c.url, p.url))),
), ),
mergeMap(({ url, name }) => mergeMap(({ url, name }) =>
this.fetchStore$(url).pipe( this.fetchStore$(url).pipe(