mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into feat/boot-param
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,9 @@
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace'
|
||||
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { CT, T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
|
||||
export module RR {
|
||||
@@ -12,7 +11,10 @@ export module RR {
|
||||
|
||||
export type WebsocketConfig<T> = Omit<WebSocketSubjectConfig<T>, 'url'>
|
||||
|
||||
// server state
|
||||
// state
|
||||
|
||||
export type EchoReq = { message: string } // server.echo
|
||||
export type EchoRes = string
|
||||
|
||||
export type ServerState = 'initializing' | 'error' | 'running'
|
||||
|
||||
@@ -225,7 +227,7 @@ export module RR {
|
||||
export type InstallPackageRes = null
|
||||
|
||||
export type GetPackageConfigReq = { id: string } // package.config.get
|
||||
export type GetPackageConfigRes = { spec: ConfigSpec; config: object }
|
||||
export type GetPackageConfigRes = { spec: CT.InputSpec; config: object }
|
||||
|
||||
export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry
|
||||
export type DrySetPackageConfigRes = Breakages
|
||||
@@ -268,14 +270,17 @@ export module RR {
|
||||
export type DryConfigureDependencyRes = {
|
||||
oldConfig: object
|
||||
newConfig: object
|
||||
spec: ConfigSpec
|
||||
spec: CT.InputSpec
|
||||
}
|
||||
|
||||
export type SideloadPackageReq = {
|
||||
manifest: T.Manifest
|
||||
icon: string // base64
|
||||
}
|
||||
export type SideloadPacakgeRes = string //guid
|
||||
export type SideloadPackageRes = {
|
||||
upload: string // guid
|
||||
progress: string // guid
|
||||
}
|
||||
|
||||
// marketplace
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ export abstract class ApiService {
|
||||
// for sideloading packages
|
||||
abstract uploadPackage(guid: string, body: Blob): Promise<string>
|
||||
|
||||
abstract uploadFile(body: Blob): Promise<string>
|
||||
|
||||
// websocket
|
||||
|
||||
abstract openWebsocket$<T>(
|
||||
@@ -17,7 +19,9 @@ export abstract class ApiService {
|
||||
config: RR.WebsocketConfig<T>,
|
||||
): Observable<T>
|
||||
|
||||
// server state
|
||||
// state
|
||||
|
||||
abstract echo(params: RR.EchoReq, url: string): Promise<RR.EchoRes>
|
||||
|
||||
abstract getState(): Promise<RR.ServerState>
|
||||
|
||||
@@ -241,7 +245,5 @@ export abstract class ApiService {
|
||||
params: RR.DryConfigureDependencyReq,
|
||||
): Promise<RR.DryConfigureDependencyRes>
|
||||
|
||||
abstract sideloadPackage(
|
||||
params: RR.SideloadPackageReq,
|
||||
): Promise<RR.SideloadPacakgeRes>
|
||||
abstract sideloadPackage(): Promise<RR.SideloadPackageRes>
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
@@ -16,7 +17,7 @@ import { Observable, filter, firstValueFrom } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { DataModel } from '../patch-db/data-model'
|
||||
import { PatchDB, pathFromArray } from 'patch-db-client'
|
||||
import { Dump, pathFromArray } from 'patch-db-client'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
@@ -25,7 +26,7 @@ export class LiveApiService extends ApiService {
|
||||
private readonly http: HttpService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly auth: AuthService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
@Inject(PATCH_CACHE) private readonly cache$: Observable<Dump<DataModel>>,
|
||||
) {
|
||||
super()
|
||||
; (window as any).rpcClient = this
|
||||
@@ -52,6 +53,15 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async uploadFile(body: Blob): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.POST,
|
||||
body,
|
||||
url: `/rest/upload`,
|
||||
responseType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
// websocket
|
||||
|
||||
openWebsocket$<T>(
|
||||
@@ -70,6 +80,10 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// state
|
||||
|
||||
async echo(params: RR.EchoReq, url: string): Promise<RR.EchoRes> {
|
||||
return this.rpcRequest({ method: 'echo', params }, url)
|
||||
}
|
||||
|
||||
async getState(): Promise<RR.ServerState> {
|
||||
return this.rpcRequest({ method: 'state', params: {} })
|
||||
}
|
||||
@@ -457,12 +471,10 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async sideloadPackage(
|
||||
params: RR.SideloadPackageReq,
|
||||
): Promise<RR.SideloadPacakgeRes> {
|
||||
async sideloadPackage(): Promise<RR.SideloadPackageRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.sideload',
|
||||
params,
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -484,7 +496,7 @@ export class LiveApiService extends ApiService {
|
||||
const patchSequence = res.headers.get('x-patch-sequence')
|
||||
if (patchSequence)
|
||||
await firstValueFrom(
|
||||
this.patch.cache$.pipe(filter(({ id }) => id >= Number(patchSequence))),
|
||||
this.cache$.pipe(filter(({ id }) => id >= Number(patchSequence))),
|
||||
)
|
||||
|
||||
return body.result
|
||||
|
||||
@@ -118,7 +118,17 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// server state
|
||||
// state
|
||||
|
||||
async echo(params: RR.EchoReq, url: string): Promise<RR.EchoRes> {
|
||||
if (url) {
|
||||
const num = Math.floor(Math.random() * 10) + 1
|
||||
if (num > 8) return params.message
|
||||
throw new Error()
|
||||
}
|
||||
await pauseFor(2000)
|
||||
return params.message
|
||||
}
|
||||
|
||||
private stateIndex = 0
|
||||
async getState(): Promise<RR.ServerState> {
|
||||
@@ -758,7 +768,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
config: Mock.MockConfig,
|
||||
spec: Mock.ConfigSpec,
|
||||
spec: await Mock.getInputSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1048,15 +1058,21 @@ export class MockApiService extends ApiService {
|
||||
return {
|
||||
oldConfig: Mock.MockConfig,
|
||||
newConfig: Mock.MockDependencyConfig,
|
||||
spec: Mock.ConfigSpec,
|
||||
spec: await Mock.getInputSpec(),
|
||||
}
|
||||
}
|
||||
|
||||
async sideloadPackage(
|
||||
params: RR.SideloadPackageReq,
|
||||
): Promise<RR.SideloadPacakgeRes> {
|
||||
async sideloadPackage(): Promise<RR.SideloadPackageRes> {
|
||||
await pauseFor(2000)
|
||||
return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated
|
||||
return {
|
||||
upload: '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated
|
||||
progress: '5120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(body: Blob): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
return 'returnedhash'
|
||||
}
|
||||
|
||||
private async initProgress(): Promise<T.FullProgress> {
|
||||
|
||||
@@ -127,7 +127,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {}, // @TODO
|
||||
actions: {},
|
||||
serviceInterfaces: {
|
||||
ui: {
|
||||
id: 'ui',
|
||||
@@ -185,7 +185,107 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
currentDependencies: {},
|
||||
hosts: {},
|
||||
hosts: {
|
||||
abcdefg: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.1',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:729CD]',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:1234]',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'bitcoin-p2p.onion',
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
|
||||
41
web/projects/ui/src/app/services/form-dialog.service.ts
Normal file
41
web/projects/ui/src/app/services/form-dialog.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { inject, Injectable, Injector, Type } from '@angular/core'
|
||||
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
|
||||
import { TuiDialogFormService, TuiPromptData } from '@taiga-ui/kit'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
|
||||
const PROMPT: Partial<TuiDialogOptions<TuiPromptData>> = {
|
||||
label: 'Unsaved Changes',
|
||||
data: {
|
||||
content: 'You have unsaved changes. Are you sure you want to leave?',
|
||||
yes: 'Leave',
|
||||
no: 'Cancel',
|
||||
},
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FormDialogService {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly formService = new TuiDialogFormService(this.dialogs)
|
||||
private readonly prompt = this.formService.withPrompt(PROMPT)
|
||||
private readonly injector = Injector.create({
|
||||
parent: inject(Injector),
|
||||
providers: [
|
||||
{
|
||||
provide: TuiDialogFormService,
|
||||
useValue: this.formService,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
open<T>(component: Type<any>, options: Partial<TuiDialogOptions<T>> = {}) {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(component, this.injector), {
|
||||
closeable: this.prompt,
|
||||
dismissible: this.prompt,
|
||||
...options,
|
||||
})
|
||||
.subscribe({
|
||||
complete: () => this.formService.markAsPristine(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,24 +7,7 @@ import {
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
import {
|
||||
ConfigSpec,
|
||||
isValueSpecListOf,
|
||||
ListValueSpecNumber,
|
||||
ListValueSpecObject,
|
||||
ListValueSpecOf,
|
||||
ListValueSpecString,
|
||||
ListValueSpecUnion,
|
||||
UniqueBy,
|
||||
ValueSpec,
|
||||
ValueSpecEnum,
|
||||
ValueSpecList,
|
||||
ValueSpecNumber,
|
||||
ValueSpecObject,
|
||||
ValueSpecString,
|
||||
ValueSpecUnion,
|
||||
} from 'src/app/pkg-config/config-types'
|
||||
import { getDefaultString, Range } from '../pkg-config/config-utilities'
|
||||
import { CT, utils } from '@start9labs/start-sdk'
|
||||
const Mustache = require('mustache')
|
||||
|
||||
@Injectable({
|
||||
@@ -34,55 +17,54 @@ export class FormService {
|
||||
constructor(private readonly formBuilder: UntypedFormBuilder) {}
|
||||
|
||||
createForm(
|
||||
spec: ConfigSpec,
|
||||
current: { [key: string]: any } = {},
|
||||
spec: CT.InputSpec,
|
||||
current: Record<string, any> = {},
|
||||
): UntypedFormGroup {
|
||||
return this.getFormGroup(spec, [], current)
|
||||
}
|
||||
|
||||
getUnionObject(
|
||||
spec: ValueSpecUnion | ListValueSpecUnion,
|
||||
selection: string,
|
||||
current?: { [key: string]: any } | null,
|
||||
): UntypedFormGroup {
|
||||
const { variants, tag } = spec
|
||||
const { name, description, warning, 'variant-names': variantNames } = tag
|
||||
|
||||
const enumSpec: ValueSpecEnum = {
|
||||
type: 'enum',
|
||||
name,
|
||||
description,
|
||||
warning,
|
||||
getUnionSelectSpec(
|
||||
spec: CT.ValueSpecUnion,
|
||||
selection: string | null,
|
||||
): CT.ValueSpecSelect {
|
||||
return {
|
||||
...spec,
|
||||
type: 'select',
|
||||
default: selection,
|
||||
values: Object.keys(variants),
|
||||
'value-names': variantNames,
|
||||
values: Object.fromEntries(
|
||||
Object.entries(spec.variants).map(([key, { name }]) => [key, name]),
|
||||
),
|
||||
}
|
||||
return this.getFormGroup(
|
||||
{ [spec.tag.id]: enumSpec, ...spec.variants[selection] },
|
||||
[],
|
||||
current,
|
||||
}
|
||||
|
||||
getUnionObject(
|
||||
spec: CT.ValueSpecUnion,
|
||||
selected: string | null,
|
||||
): UntypedFormGroup {
|
||||
const group = this.getFormGroup({
|
||||
selection: this.getUnionSelectSpec(spec, selected),
|
||||
})
|
||||
|
||||
group.setControl(
|
||||
'value',
|
||||
this.getFormGroup(selected ? spec.variants[selected].spec : {}),
|
||||
)
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
getListItem(spec: ValueSpecList, entry: any) {
|
||||
const listItemValidators = getListItemValidators(spec)
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return this.formBuilder.control(entry, listItemValidators)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
return this.formBuilder.control(entry, listItemValidators)
|
||||
} else if (isValueSpecListOf(spec, 'enum')) {
|
||||
return this.formBuilder.control(entry)
|
||||
} else if (isValueSpecListOf(spec, 'object')) {
|
||||
return this.getFormGroup(spec.spec.spec, listItemValidators, entry)
|
||||
} else if (isValueSpecListOf(spec, 'union')) {
|
||||
return this.getUnionObject(spec.spec, spec.spec.default, entry)
|
||||
getListItem(spec: CT.ValueSpecList, entry?: any) {
|
||||
if (CT.isValueSpecListOf(spec, 'text')) {
|
||||
return this.formBuilder.control(entry, stringValidators(spec.spec))
|
||||
} else if (CT.isValueSpecListOf(spec, 'object')) {
|
||||
return this.getFormGroup(spec.spec.spec, [], entry)
|
||||
}
|
||||
}
|
||||
|
||||
private getFormGroup(
|
||||
config: ConfigSpec,
|
||||
getFormGroup(
|
||||
config: CT.InputSpec,
|
||||
validators: ValidatorFn[] = [],
|
||||
current?: { [key: string]: any } | null,
|
||||
current?: Record<string, any> | null,
|
||||
): UntypedFormGroup {
|
||||
let group: Record<
|
||||
string,
|
||||
@@ -95,150 +77,281 @@ export class FormService {
|
||||
}
|
||||
|
||||
private getFormEntry(
|
||||
spec: ValueSpec,
|
||||
spec: CT.ValueSpec,
|
||||
currentValue?: any,
|
||||
): UntypedFormGroup | UntypedFormArray | UntypedFormControl {
|
||||
let validators: ValidatorFn[]
|
||||
let value: any
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
validators = stringValidators(spec)
|
||||
case 'text':
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
value = spec.default ? getDefaultString(spec.default) : null
|
||||
value = spec.default ? utils.getDefaultString(spec.default) : null
|
||||
}
|
||||
return this.formBuilder.control(value, validators)
|
||||
return this.formBuilder.control(value, stringValidators(spec))
|
||||
case 'textarea':
|
||||
value = currentValue || null
|
||||
return this.formBuilder.control(value, textareaValidators(spec))
|
||||
case 'number':
|
||||
validators = numberValidators(spec)
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
value = spec.default || null
|
||||
}
|
||||
return this.formBuilder.control(value, validators)
|
||||
return this.formBuilder.control(value, numberValidators(spec))
|
||||
case 'color':
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
value = spec.default || null
|
||||
}
|
||||
return this.formBuilder.control(value, colorValidators(spec))
|
||||
case 'datetime':
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
value = spec.default || null
|
||||
}
|
||||
return this.formBuilder.control(value, datetimeValidators(spec))
|
||||
case 'object':
|
||||
return this.getFormGroup(spec.spec, [], currentValue)
|
||||
case 'list':
|
||||
validators = listValidators(spec)
|
||||
const mapped = (
|
||||
Array.isArray(currentValue) ? currentValue : (spec.default as any[])
|
||||
).map(entry => {
|
||||
return this.getListItem(spec, entry)
|
||||
})
|
||||
return this.formBuilder.array(mapped, validators)
|
||||
return this.formBuilder.array(mapped, listValidators(spec))
|
||||
case 'file':
|
||||
return this.formBuilder.control(
|
||||
currentValue || null,
|
||||
fileValidators(spec),
|
||||
)
|
||||
case 'union':
|
||||
const currentSelection = currentValue?.[spec.tag.id]
|
||||
const currentSelection = currentValue?.selection
|
||||
const isValid = !!spec.variants[currentSelection]
|
||||
|
||||
return this.getUnionObject(
|
||||
spec,
|
||||
isValid ? currentSelection : spec.default,
|
||||
isValid ? currentValue : undefined,
|
||||
)
|
||||
case 'boolean':
|
||||
case 'enum':
|
||||
case 'toggle':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value)
|
||||
case 'select':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value, selectValidators(spec))
|
||||
case 'multiselect':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value, multiselectValidators(spec))
|
||||
default:
|
||||
return this.formBuilder.control(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getListItemValidators(spec: ValueSpecList) {
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return stringValidators(spec.spec)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
return numberValidators(spec.spec)
|
||||
}
|
||||
}
|
||||
// function getListItemValidators(spec: CT.ValueSpecList) {
|
||||
// if (CT.isValueSpecListOf(spec, 'text')) {
|
||||
// return stringValidators(spec.spec)
|
||||
// }
|
||||
// }
|
||||
|
||||
function stringValidators(
|
||||
spec: ValueSpecString | ListValueSpecString,
|
||||
spec: CT.ValueSpecText | CT.ListValueSpecText,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (!(spec as ValueSpecString).nullable) {
|
||||
if ((spec as CT.ValueSpecText).required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
if (spec.pattern) {
|
||||
validators.push(Validators.pattern(spec.pattern))
|
||||
validators.push(textLengthInRange(spec.minLength, spec.maxLength))
|
||||
|
||||
if (spec.patterns.length) {
|
||||
spec.patterns.forEach(p => validators.push(Validators.pattern(p.regex)))
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function numberValidators(
|
||||
spec: ValueSpecNumber | ListValueSpecNumber,
|
||||
): ValidatorFn[] {
|
||||
function textareaValidators(spec: CT.ValueSpecTextarea): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (spec.required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
validators.push(textLengthInRange(spec.minLength, spec.maxLength))
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function colorValidators({ required }: CT.ValueSpecColor): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = [Validators.pattern(/^#[0-9a-f]{6}$/i)]
|
||||
|
||||
if (required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function datetimeValidators({
|
||||
required,
|
||||
min,
|
||||
max,
|
||||
}: CT.ValueSpecDatetime): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
if (min) {
|
||||
validators.push(datetimeMin(min))
|
||||
}
|
||||
|
||||
if (max) {
|
||||
validators.push(datetimeMax(max))
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function numberValidators(spec: CT.ValueSpecNumber): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(isNumber())
|
||||
|
||||
if (!(spec as ValueSpecNumber).nullable) {
|
||||
if ((spec as CT.ValueSpecNumber).required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
if (spec.integral) {
|
||||
if (spec.integer) {
|
||||
validators.push(isInteger())
|
||||
}
|
||||
|
||||
validators.push(numberInRange(spec.range))
|
||||
validators.push(numberInRange(spec.min, spec.max))
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function listValidators(spec: ValueSpecList): ValidatorFn[] {
|
||||
function selectValidators(spec: CT.ValueSpecSelect): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(listInRange(spec.range))
|
||||
|
||||
validators.push(listItemIssue())
|
||||
|
||||
if (!isValueSpecListOf(spec, 'enum')) {
|
||||
validators.push(listUnique(spec))
|
||||
if (spec.required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
export function numberInRange(stringRange: string): ValidatorFn {
|
||||
function multiselectValidators(spec: CT.ValueSpecMultiselect): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
validators.push(listInRange(spec.minLength, spec.maxLength))
|
||||
return validators
|
||||
}
|
||||
|
||||
function listValidators(spec: CT.ValueSpecList): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
validators.push(listInRange(spec.minLength, spec.maxLength))
|
||||
validators.push(listItemIssue())
|
||||
return validators
|
||||
}
|
||||
|
||||
function fileValidators(spec: CT.ValueSpecFile): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (spec.required) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
export function numberInRange(
|
||||
min: number | null,
|
||||
max: number | null,
|
||||
): ValidatorFn {
|
||||
return control => {
|
||||
const value = control.value
|
||||
if (!value) return null
|
||||
try {
|
||||
Range.from(stringRange).checkIncludes(value)
|
||||
return null
|
||||
} catch (e: any) {
|
||||
return { numberNotInRange: { value: `Number must be ${e.message}` } }
|
||||
}
|
||||
if (typeof value !== 'number') return null
|
||||
if (min && value < min)
|
||||
return {
|
||||
numberNotInRange: `Number must be greater than or equal to ${min}`,
|
||||
}
|
||||
if (max && value > max)
|
||||
return { numberNotInRange: `Number must be less than or equal to ${max}` }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function isNumber(): ValidatorFn {
|
||||
return control =>
|
||||
!control.value || control.value == Number(control.value)
|
||||
? null
|
||||
: { notNumber: { value: control.value } }
|
||||
return ({ value }) =>
|
||||
!value || value == Number(value) ? null : { notNumber: 'Must be a number' }
|
||||
}
|
||||
|
||||
export function isInteger(): ValidatorFn {
|
||||
return control =>
|
||||
!control.value || control.value == Math.trunc(control.value)
|
||||
return ({ value }) =>
|
||||
!value || value == Math.trunc(value)
|
||||
? null
|
||||
: { numberNotInteger: { value: control.value } }
|
||||
: { numberNotInteger: 'Must be an integer' }
|
||||
}
|
||||
|
||||
export function listInRange(stringRange: string): ValidatorFn {
|
||||
export function listInRange(
|
||||
minLength: number | null,
|
||||
maxLength: number | null,
|
||||
): ValidatorFn {
|
||||
return control => {
|
||||
try {
|
||||
Range.from(stringRange).checkIncludes(control.value.length)
|
||||
return null
|
||||
} catch (e: any) {
|
||||
return { listNotInRange: { value: `List must be ${e.message}` } }
|
||||
}
|
||||
const length = control.value.length
|
||||
if (minLength && length < minLength)
|
||||
return {
|
||||
listNotInRange: `List must contain at least ${minLength} entries`,
|
||||
}
|
||||
if (maxLength && length > maxLength)
|
||||
return {
|
||||
listNotInRange: `List cannot contain more than ${maxLength} entries`,
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function datetimeMin(min: string): ValidatorFn {
|
||||
return ({ value }) => {
|
||||
if (!value) return null
|
||||
|
||||
const date = new Date(value.length === 5 ? `2000-01-01T${value}` : value)
|
||||
const minDate = new Date(min.length === 5 ? `2000-01-01T${min}` : min)
|
||||
|
||||
return date < minDate ? { datetimeMin: `Minimum is ${min}` } : null
|
||||
}
|
||||
}
|
||||
|
||||
export function datetimeMax(max: string): ValidatorFn {
|
||||
return ({ value }) => {
|
||||
if (!value) return null
|
||||
|
||||
const date = new Date(value.length === 5 ? `2000-01-01T${value}` : value)
|
||||
const maxDate = new Date(max.length === 5 ? `2000-01-01T${max}` : max)
|
||||
|
||||
return date > maxDate ? { datetimeMin: `Maximum is ${max}` } : null
|
||||
}
|
||||
}
|
||||
|
||||
export function textLengthInRange(
|
||||
minLength: number | null,
|
||||
maxLength: number | null,
|
||||
): ValidatorFn {
|
||||
return control => {
|
||||
const value = control.value
|
||||
if (value === null || value === undefined) return null
|
||||
|
||||
const length = value.length
|
||||
if (minLength && length < minLength)
|
||||
return { listNotInRange: `Must be at least ${minLength} characters` }
|
||||
if (maxLength && length > maxLength)
|
||||
return { listNotInRange: `Cannot be great than ${maxLength} characters` }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,36 +360,33 @@ export function listItemIssue(): ValidatorFn {
|
||||
const { controls } = parentControl as UntypedFormArray
|
||||
const problemChild = controls.find(c => c.invalid)
|
||||
if (problemChild) {
|
||||
return { listItemIssue: { value: 'Invalid entries' } }
|
||||
return { listItemIssue: 'Invalid entries' }
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listUnique(spec: ValueSpecList): ValidatorFn {
|
||||
export function listUnique(spec: CT.ValueSpecList): ValidatorFn {
|
||||
return control => {
|
||||
const list = control.value
|
||||
for (let idx = 0; idx < list.length; idx++) {
|
||||
for (let idx2 = idx + 1; idx2 < list.length; idx2++) {
|
||||
if (listItemEquals(spec, list[idx], list[idx2])) {
|
||||
const objSpec = spec.spec
|
||||
let display1: string
|
||||
let display2: string
|
||||
let uniqueMessage = isObjectOrUnion(spec.spec)
|
||||
? uniqueByMessageWrapper(
|
||||
spec.spec['unique-by'],
|
||||
spec.spec,
|
||||
list[idx],
|
||||
)
|
||||
let uniqueMessage = isObject(objSpec)
|
||||
? uniqueByMessageWrapper(objSpec.uniqueBy, objSpec)
|
||||
: ''
|
||||
|
||||
if (isObjectOrUnion(spec.spec) && spec.spec['display-as']) {
|
||||
if (isObject(objSpec) && objSpec.displayAs) {
|
||||
display1 = `"${(Mustache as any).render(
|
||||
spec.spec['display-as'],
|
||||
objSpec.displayAs,
|
||||
list[idx],
|
||||
)}"`
|
||||
display2 = `"${(Mustache as any).render(
|
||||
spec.spec['display-as'],
|
||||
objSpec.displayAs,
|
||||
list[idx2],
|
||||
)}"`
|
||||
} else {
|
||||
@@ -285,9 +395,7 @@ export function listUnique(spec: ValueSpecList): ValidatorFn {
|
||||
}
|
||||
|
||||
return {
|
||||
listNotUnique: {
|
||||
value: `${display1} and ${display2} are not unique.${uniqueMessage}`,
|
||||
},
|
||||
listNotUnique: `${display1} and ${display2} are not unique.${uniqueMessage}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,46 +404,40 @@ export function listUnique(spec: ValueSpecList): ValidatorFn {
|
||||
}
|
||||
}
|
||||
|
||||
function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean {
|
||||
function listItemEquals(spec: CT.ValueSpecList, val1: any, val2: any): boolean {
|
||||
// TODO: fix types
|
||||
switch (spec.subtype) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'enum':
|
||||
switch (spec.spec.type) {
|
||||
case 'text':
|
||||
return val1 == val2
|
||||
case 'object':
|
||||
const obj: ListValueSpecObject = spec.spec as any
|
||||
|
||||
return listObjEquals(obj['unique-by'], obj, val1, val2)
|
||||
case 'union':
|
||||
const union: ListValueSpecUnion = spec.spec as any
|
||||
|
||||
return unionEquals(union['unique-by'], union, val1, val2)
|
||||
const obj = spec.spec
|
||||
return listObjEquals(obj.uniqueBy, obj, val1, val2)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean {
|
||||
function itemEquals(spec: CT.ValueSpec, val1: any, val2: any): boolean {
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
case 'enum':
|
||||
case 'toggle':
|
||||
case 'select':
|
||||
return val1 == val2
|
||||
case 'object':
|
||||
// TODO: 'unique-by' does not exist on ValueSpecObject, fix types
|
||||
return objEquals(
|
||||
(spec as any)['unique-by'],
|
||||
spec as ValueSpecObject,
|
||||
spec as CT.ValueSpecObject,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
case 'union':
|
||||
// TODO: 'unique-by' does not exist on ValueSpecUnion, fix types
|
||||
// TODO: 'unique-by' does not exist on CT.ValueSpecUnion, fix types
|
||||
return unionEquals(
|
||||
(spec as any)['unique-by'],
|
||||
spec as ValueSpecUnion,
|
||||
spec as CT.ValueSpecUnion,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
@@ -355,12 +457,12 @@ function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean {
|
||||
}
|
||||
|
||||
function listObjEquals(
|
||||
uniqueBy: UniqueBy,
|
||||
spec: ListValueSpecObject,
|
||||
uniqueBy: CT.UniqueBy,
|
||||
spec: CT.ListValueSpecObject,
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
if (uniqueBy === null) {
|
||||
if (!uniqueBy) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return itemEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
@@ -383,12 +485,12 @@ function listObjEquals(
|
||||
}
|
||||
|
||||
function objEquals(
|
||||
uniqueBy: UniqueBy,
|
||||
spec: ValueSpecObject,
|
||||
uniqueBy: CT.UniqueBy,
|
||||
spec: CT.ValueSpecObject,
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
if (uniqueBy === null) {
|
||||
if (!uniqueBy) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
// TODO: fix types
|
||||
@@ -412,20 +514,19 @@ function objEquals(
|
||||
}
|
||||
|
||||
function unionEquals(
|
||||
uniqueBy: UniqueBy,
|
||||
spec: ValueSpecUnion | ListValueSpecUnion,
|
||||
uniqueBy: CT.UniqueBy,
|
||||
spec: CT.ValueSpecUnion,
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
const tagId = spec.tag.id
|
||||
const variant = spec.variants[val1[tagId]]
|
||||
if (uniqueBy === null) {
|
||||
const variantSpec = spec.variants[val1.selection].spec
|
||||
if (!uniqueBy) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
if (uniqueBy === tagId) {
|
||||
return val1[tagId] === val2[tagId]
|
||||
if (uniqueBy === 'selection') {
|
||||
return val1.selection === val2.selection
|
||||
} else {
|
||||
return itemEquals(variant[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
return itemEquals(variantSpec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
}
|
||||
} else if ('any' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
@@ -446,20 +547,10 @@ function unionEquals(
|
||||
}
|
||||
|
||||
function uniqueByMessageWrapper(
|
||||
uniqueBy: UniqueBy,
|
||||
spec: ListValueSpecObject | ListValueSpecUnion,
|
||||
obj: Record<string, string>,
|
||||
uniqueBy: CT.UniqueBy,
|
||||
spec: CT.ListValueSpecObject,
|
||||
) {
|
||||
let configSpec: ConfigSpec
|
||||
if (isUnion(spec)) {
|
||||
const tagId = spec.tag.id
|
||||
configSpec = {
|
||||
[tagId]: { name: spec.tag.name } as ValueSpec,
|
||||
...spec.variants[obj[tagId]],
|
||||
}
|
||||
} else {
|
||||
configSpec = spec.spec
|
||||
}
|
||||
let configSpec = spec.spec
|
||||
|
||||
const message = uniqueByMessage(uniqueBy, configSpec)
|
||||
if (message) {
|
||||
@@ -468,17 +559,17 @@ function uniqueByMessageWrapper(
|
||||
}
|
||||
|
||||
function uniqueByMessage(
|
||||
uniqueBy: UniqueBy,
|
||||
configSpec: ConfigSpec,
|
||||
uniqueBy: CT.UniqueBy,
|
||||
configSpec: CT.InputSpec,
|
||||
outermost = true,
|
||||
): string {
|
||||
let joinFunc
|
||||
const subSpecs: string[] = []
|
||||
if (uniqueBy === null) {
|
||||
if (!uniqueBy) {
|
||||
return ''
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return configSpec[uniqueBy]
|
||||
? (configSpec[uniqueBy] as ValueSpecObject).name
|
||||
? (configSpec[uniqueBy] as CT.ValueSpecObject).name
|
||||
: uniqueBy
|
||||
} else if ('any' in uniqueBy) {
|
||||
joinFunc = ' OR '
|
||||
@@ -497,20 +588,15 @@ function uniqueByMessage(
|
||||
: '(' + ret + ')'
|
||||
}
|
||||
|
||||
function isObjectOrUnion(
|
||||
spec: ListValueSpecOf<any>,
|
||||
): spec is ListValueSpecObject | ListValueSpecUnion {
|
||||
// only lists of objects and unions have unique-by
|
||||
return 'unique-by' in spec
|
||||
}
|
||||
|
||||
function isUnion(spec: any): spec is ListValueSpecUnion {
|
||||
// only unions have tag
|
||||
return !!spec.tag
|
||||
function isObject(
|
||||
spec: CT.ListValueSpecOf<any>,
|
||||
): spec is CT.ListValueSpecObject {
|
||||
// only lists of objects have uniqueBy
|
||||
return 'uniqueBy' in spec
|
||||
}
|
||||
|
||||
export function convertValuesRecursive(
|
||||
configSpec: ConfigSpec,
|
||||
configSpec: CT.InputSpec,
|
||||
group: UntypedFormGroup,
|
||||
) {
|
||||
Object.entries(configSpec).forEach(([key, valueSpec]) => {
|
||||
@@ -522,40 +608,27 @@ export function convertValuesRecursive(
|
||||
control.setValue(
|
||||
control.value || control.value === 0 ? Number(control.value) : null,
|
||||
)
|
||||
} else if (valueSpec.type === 'string') {
|
||||
} else if (valueSpec.type === 'text' || valueSpec.type === 'textarea') {
|
||||
if (!control.value) control.setValue(null)
|
||||
} else if (valueSpec.type === 'object') {
|
||||
convertValuesRecursive(valueSpec.spec, group.get(key) as UntypedFormGroup)
|
||||
} else if (valueSpec.type === 'union') {
|
||||
const formGr = group.get(key) as UntypedFormGroup
|
||||
const spec = valueSpec.variants[formGr.controls[valueSpec.tag.id].value]
|
||||
const spec = valueSpec.variants[formGr.controls['selection'].value].spec
|
||||
convertValuesRecursive(spec, formGr)
|
||||
} else if (valueSpec.type === 'list') {
|
||||
const formArr = group.get(key) as UntypedFormArray
|
||||
const { controls } = formArr
|
||||
|
||||
if (valueSpec.subtype === 'number') {
|
||||
controls.forEach(control => {
|
||||
control.setValue(control.value ? Number(control.value) : null)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'string') {
|
||||
if (valueSpec.spec.type === 'text') {
|
||||
controls.forEach(control => {
|
||||
if (!control.value) control.setValue(null)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'object') {
|
||||
} else if (valueSpec.spec.type === 'object') {
|
||||
controls.forEach(formGroup => {
|
||||
const objectSpec = valueSpec.spec as ListValueSpecObject
|
||||
const objectSpec = valueSpec.spec as CT.ListValueSpecObject
|
||||
convertValuesRecursive(objectSpec.spec, formGroup as UntypedFormGroup)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'union') {
|
||||
controls.forEach(formGroup => {
|
||||
const unionSpec = valueSpec.spec as ListValueSpecUnion
|
||||
const spec =
|
||||
unionSpec.variants[
|
||||
(formGroup as UntypedFormGroup).controls[unionSpec.tag.id].value
|
||||
]
|
||||
convertValuesRecursive(spec, formGroup as UntypedFormGroup)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ModalService {
|
||||
constructor(private readonly modalCtrl: ModalController) {}
|
||||
|
||||
async presentModalConfig(componentProps: ComponentProps): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppConfigPage,
|
||||
componentProps,
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
|
||||
interface ComponentProps {
|
||||
pkgId: string
|
||||
dependentInfo?: DependentInfo
|
||||
}
|
||||
57
web/projects/ui/src/app/services/patch-db/patch-db-source.ts
Normal file
57
web/projects/ui/src/app/services/patch-db/patch-db-source.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { inject, Injectable, InjectionToken } from '@angular/core'
|
||||
import { Dump, Revision, Update } from 'patch-db-client'
|
||||
import { BehaviorSubject, EMPTY, Observable } from 'rxjs'
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
filter,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs/operators'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DataModel } from './data-model'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
|
||||
export const PATCH_CACHE = new InjectionToken('', {
|
||||
factory: () =>
|
||||
new BehaviorSubject<Dump<DataModel>>({
|
||||
id: 0,
|
||||
value: {} as DataModel,
|
||||
}),
|
||||
})
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchDbSource extends Observable<Update<DataModel>[]> {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly state = inject(StateService)
|
||||
private readonly stream$ = inject(AuthService).isVerified$.pipe(
|
||||
switchMap(verified => (verified ? this.api.subscribeToPatchDB({}) : EMPTY)),
|
||||
switchMap(({ dump, guid }) =>
|
||||
this.api.openWebsocket$<Revision>(guid, {}).pipe(
|
||||
bufferTime(250),
|
||||
filter(revisions => !!revisions.length),
|
||||
startWith([dump]),
|
||||
),
|
||||
),
|
||||
catchError((_, original$) => {
|
||||
this.state.retrigger()
|
||||
|
||||
// @TODO this is returning right away, but we need to wait until state emits again from the retrigger() above.
|
||||
return this.state.pipe(
|
||||
filter(current => current === 'running'),
|
||||
take(1),
|
||||
switchMap(() => original$),
|
||||
)
|
||||
}),
|
||||
startWith([inject(LocalStorageBootstrap).init()]),
|
||||
)
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { InjectionToken, Injector } from '@angular/core'
|
||||
import { Revision, Update } from 'patch-db-client'
|
||||
import { defer, EMPTY, from, Observable } from 'rxjs'
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
filter,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs/operators'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DataModel } from './data-model'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
|
||||
export const PATCH_SOURCE = new InjectionToken<Observable<Update<DataModel>[]>>(
|
||||
'',
|
||||
)
|
||||
|
||||
export function sourceFactory(
|
||||
injector: Injector,
|
||||
): Observable<Update<DataModel>[]> {
|
||||
// defer() needed to avoid circular dependency with ApiService, since PatchDB is needed there
|
||||
return defer(() => {
|
||||
const api = injector.get(ApiService)
|
||||
const auth = injector.get(AuthService)
|
||||
const state = injector.get(StateService)
|
||||
const bootstrapper = injector.get(LocalStorageBootstrap)
|
||||
|
||||
return auth.isVerified$.pipe(
|
||||
switchMap(verified =>
|
||||
verified ? from(api.subscribeToPatchDB({})) : EMPTY,
|
||||
),
|
||||
switchMap(({ dump, guid }) =>
|
||||
api.openWebsocket$<Revision>(guid, {}).pipe(
|
||||
bufferTime(250),
|
||||
filter(revisions => !!revisions.length),
|
||||
startWith([dump]),
|
||||
),
|
||||
),
|
||||
catchError((_, original$) => {
|
||||
state.retrigger()
|
||||
|
||||
return state.pipe(
|
||||
filter(current => current === 'running'),
|
||||
take(1),
|
||||
switchMap(() => original$),
|
||||
)
|
||||
}),
|
||||
startWith([bootstrapper.init()]),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Injector, NgModule } from '@angular/core'
|
||||
import { PATCH_SOURCE, sourceFactory } from './patch-db.factory'
|
||||
|
||||
// This module is purely for providers organization purposes
|
||||
@NgModule({
|
||||
providers: [
|
||||
{
|
||||
provide: PATCH_SOURCE,
|
||||
deps: [Injector],
|
||||
useFactory: sourceFactory,
|
||||
},
|
||||
{
|
||||
provide: PatchDB,
|
||||
deps: [PATCH_SOURCE],
|
||||
useClass: PatchDB,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class PatchDbModule {}
|
||||
Reference in New Issue
Block a user