0.3.0 refactor

ui: adds overlay layer to patch-db-client

ui: getting towards mocks

ui: cleans up factory init

ui: nice type hack

ui: live api for patch

ui: api service source + http

starts up

ui: api source + http

ui: rework patchdb config, pass stashTimeout into patchDbModel

wires in temp patching into api service

ui: example of wiring patchdbmodel into page

begin integration

remove unnecessary method

linting

first data rendering

rework app initialization

http source working for ssh delete call

temp patches working

entire Embassy tab complete

not in kansas anymore

ripping, saving progress

progress for API request response types and endoint defs

Update data-model.ts

shambles, but in a good way

progress

big progress

progress

installed list working

big progress

progress

progress

begin marketplace redesign

Update api-types.ts

Update api-types.ts

marketplace improvements

cosmetic

dependencies and recommendations

begin nym auth approach

install wizard

restore flow and donations
This commit is contained in:
Aaron Greenspan
2021-02-16 13:45:09 -07:00
committed by Aiden McClelland
parent 46f32cb90b
commit 8d01ebe8b2
238 changed files with 25509 additions and 14852 deletions

View File

@@ -0,0 +1,483 @@
import { ValueSpec, ConfigSpec, UniqueBy, ValueSpecOf, ValueType, ValueSpecObject, ValueSpecUnion } from './config-types'
import { Annotations, getDefaultObject, getDefaultUnion, listInnerSpec, mapConfigSpec, Range } from './config-utilities'
import * as pointer from 'json-pointer'
import * as handlebars from 'handlebars'
export class ConfigCursor<T extends ValueType> {
private cachedSpec?: ValueSpecOf<T>
constructor (
private readonly rootSpec: ConfigSpec,
private readonly rootOldConfig: object,
private readonly rootMappedConfig: object = null,
private readonly rootConfig: object = null,
private readonly ptr: string = '',
) {
if (!this.rootOldConfig) {
this.rootOldConfig = getDefaultObject(this.rootSpec)
}
if (!this.rootMappedConfig) {
this.rootMappedConfig = JSON.parse(JSON.stringify(this.rootOldConfig))
mapConfigSpec(this.rootSpec, this.rootMappedConfig)
}
if (!this.rootConfig) {
this.rootConfig = JSON.parse(JSON.stringify(this.rootMappedConfig))
}
}
seek<S extends ValueType> (ptr: string): ConfigCursor<S> {
return new ConfigCursor(
this.rootSpec,
this.rootOldConfig,
this.rootMappedConfig,
this.rootConfig,
pointer.compile(
pointer.parse(this.ptr)
.concat(pointer.parse(ptr)),
),
)
}
seekNext<S extends ValueType> (key: string | number): ConfigCursor<S> {
return this.seek(pointer.compile([`${key}`]))
}
unseek<S extends ValueType> (levels?: number): ConfigCursor<S> {
let ptr: string
if (levels === undefined) {
ptr = ''
} else {
// TODO, delete or make use of, it isn't being used so far
// This is not being used so far
let ptr_arr = pointer.parse(this.ptr)
for (let i = 0; i < levels; i++) {
ptr_arr.pop()
}
ptr = pointer.compile(ptr_arr)
}
return new ConfigCursor(
this.rootSpec,
this.rootOldConfig,
this.rootMappedConfig,
this.rootConfig,
ptr,
)
}
key (): string {
return pointer.parse(this.ptr).pop()
}
oldConfig (): any {
if (pointer.has(this.rootOldConfig, this.ptr)) {
return pointer.get(this.rootOldConfig, this.ptr)
} else {
return undefined
}
}
mappedConfig (): any {
if (pointer.has(this.rootMappedConfig, this.ptr)) {
return pointer.get(this.rootMappedConfig, this.ptr)
} else {
return undefined
}
}
toString (): string {
const spec: ValueSpec = this.spec()
const config = this.config()
switch (spec.type) {
case 'string':
return config
case 'number':
return `${config}${spec.units ? ' ' + spec.units : ''}`
case 'object':
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : ''
case 'union':
return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : config[spec.tag.id]
case 'pointer':
return 'System Defined'
default:
return ''
}
}
// if (config : T) then (spec : ValueSpecOf<T>)
config (): any {
if (pointer.has(this.rootConfig, this.ptr)) {
return pointer.get(this.rootConfig, this.ptr)
} else {
return undefined
}
}
// if (config : T) then (spec : ValueSpecOf<T>)
spec (): ValueSpecOf<T> {
if (this.cachedSpec) return this.cachedSpec
const parsed = pointer.parse(this.ptr)
// We elevate the rootSpec (ConfigSpec) to a dummy ValueSpecObject
let ret: ValueSpec = {
type: 'object',
spec: this.rootSpec,
nullable: false,
nullByDefault: false,
name: 'Config',
displayAs: 'Config',
uniqueBy: null,
}
let ptr = []
for (let seg of parsed) {
switch (ret.type) {
case 'object':
ret = ret.spec[seg]
break
case 'union':
if (seg === ret.tag.id) {
ret = {
type: 'enum',
default: ret.default,
values: Object.keys(ret.variants),
name: ret.tag.name,
description: ret.tag.description,
valueNames: ret.tag.variantNames,
}
} else {
const cfg = this.unseek().seek(pointer.compile(ptr))
ret = ret.variants[cfg.config()[ret.tag.id]][seg]
}
break
case 'list':
//in essence, for a list we replace the list typed ValueSpecOf with it's internal ListValueSpec, a ValueSpecOf<T> where config @ ptr is of type T[].
// we also append default values to it.
// note also that jsonKey is not used. jsonKey in this case is an index of an array, like 0, 1, etc.
// this implies that every index of a list has an identical inner spec
ret = listInnerSpec(ret)
break
default:
return undefined
}
if (ret === undefined) break
ptr.push(seg)
}
this.cachedSpec = ret as ValueSpecOf<T>
return this.cachedSpec
}
checkInvalid (): string | null { // null if valid
const spec: ValueSpec = this.spec()
const cfg = this.config()
switch (spec.type) {
case 'string':
if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.`
} else if (typeof cfg === 'string') {
if (!spec.pattern || new RegExp(spec.pattern).test(cfg)) {
return null
} else {
return spec.patternDescription
}
} else {
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'number':
if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.`
} else if (typeof cfg === 'number') {
if (spec.integral && cfg != Math.trunc(cfg)) {
return `${spec.name} must be an integer.`
}
try {
Range.from(spec.range).checkIncludes(cfg)
return null
} catch (e) {
return e.message
}
} else {
throw new TypeError(`${this.ptr}: expected number, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'boolean':
if (typeof cfg === 'boolean') {
return null
} else {
throw new TypeError(`${this.ptr}: expected boolean, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'enum':
if (typeof cfg === 'string') {
spec.valuesSet = spec.valuesSet || new Set(spec.values)
return spec.valuesSet.has(cfg) ? null : `${cfg} is not a valid selection.`
} else {
throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'list':
if (Array.isArray(cfg)) {
const range = Range.from(spec.range)
const min = range.integralMin()
const max = range.integralMax()
const length = cfg.length
if (min && length < min) {
return spec.subtype === 'enum' ? 'Not enough options selected.' : 'List is too short.'
}
if (max && length > max) {
return spec.subtype === 'enum' ? 'Too many options selected.' : 'List is too long.'
}
for (let idx = 0; idx < cfg.length; idx++) {
let cursor = this.seekNext(idx)
const invalid = cursor.checkInvalid()
if (invalid) {
return `Item #${idx + 1} is invalid. ${invalid}.`
}
if (spec.subtype === 'enum') continue
for (let idx2 = idx + 1; idx2 < cfg.length; idx2++) {
if (cursor.equals(this.seekNext(idx2))) {
return `Item #${idx + 1} is not unique.` + ('uniqueBy' in cursor.spec()) ? `${
displayUniqueBy(
(cursor.spec() as ValueSpecObject | ValueSpecUnion).uniqueBy,
(cursor.spec() as ValueSpecObject | ValueSpecUnion),
cursor.config(),
)
} must be unique.` : ''
}
}
}
return null
} else {
throw new TypeError(`${this.ptr}: expected array, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'object':
if (!cfg) {
return spec.nullable ? null : `${spec.name} is missing.`
} else if (typeof cfg === 'object' && !Array.isArray(cfg)) {
for (let idx in spec.spec) {
if (this.seekNext(idx).checkInvalid()) {
return `${spec.spec[idx].name} is invalid.`
}
}
return null
} else {
throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
case 'pointer':
return null
case 'union':
if (typeof cfg === 'object' && !Array.isArray(cfg)) {
if (typeof cfg[spec.tag.id] === 'string') {
for (let idx in spec.variants[cfg[spec.tag.id]]) {
if (this.seekNext(idx).checkInvalid()) {
return `${spec.variants[cfg[spec.tag.id]][idx].name} is invalid.`
}
}
return null
} else {
throw new TypeError(`${this.ptr}/${spec.tag.id}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
} else {
throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`)
}
}
}
isNew (): boolean {
const oldCfg = this.oldConfig()
const mappedCfg = this.mappedConfig()
if (mappedCfg && oldCfg && typeof mappedCfg === 'object' && typeof oldCfg === 'object') {
for (let key in mappedCfg) {
if (this.seekNext(key).isNew()) return true
}
return false
} else {
return mappedCfg !== oldCfg
}
}
isEdited (): boolean {
const cfg = this.config()
const mappedCfg = this.mappedConfig()
if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') {
const spec = this.spec()
let allKeys: Set<string>
if (spec.type === 'union') {
let unionSpec = spec as ValueSpecOf<'union'>
const labelForSelection = unionSpec.tag.id
allKeys = new Set([labelForSelection, ...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
} else {
allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)])
}
for (let key of allKeys) {
if (this.seekNext(key).isEdited()) return true
}
return false
} else {
return cfg !== mappedCfg
}
}
equals (cursor: ConfigCursor<T>): boolean {
const lhs = this.config()
const rhs = cursor.config()
const spec: ValueSpec = this.spec()
switch (spec.type) {
case 'string':
case 'number':
case 'boolean':
case 'enum':
return lhs === rhs
case 'object':
case 'union':
return isEqual(spec.uniqueBy, this as ConfigCursor<'object' | 'union'>, cursor as ConfigCursor<'object' | 'union'>)
case 'list':
if (lhs.length !== rhs.length) {
return false
}
for (let idx = 0; idx < lhs.length; idx++) {
if (!this.seekNext(`${idx}`).equals(cursor.seekNext(`${idx}`))) {
return false
}
}
return true
default:
return false
}
}
getAnnotations (): Annotations<T> {
const spec: ValueSpec = this.spec()
switch (spec.type) {
case 'object': {
const ret: Annotations<'object'> = {
self: {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
},
members: { },
}
for (let key in spec.spec) {
let annotation: any = this.seekNext(key).getAnnotations()
if ('self' in annotation) {
annotation = annotation.self
}
ret.members[key] = annotation
}
return ret as Annotations<T>
}
case 'union': {
const ret: Annotations<'union'> = {
self: {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
},
members: {
[spec.tag.id]: this.seekNext<'enum'>(spec.tag.id).getAnnotations(),
},
}
for (let key in spec.variants[this.config()[spec.tag.id]]) {
let annotation: any = this.seekNext(key).getAnnotations()
if ('self' in annotation) {
annotation = annotation.self
}
ret.members[key] = annotation
}
return ret as Annotations<T>
}
case 'list': {
const ret: Annotations<'list'> = {
self: {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
},
members: [],
}
for (let key in this.config()) {
let annotation: any = this.seekNext(key).getAnnotations()
if ('self' in annotation) {
annotation = annotation.self
}
ret.members[key] = annotation
}
return ret as Annotations<T>
}
default:
return {
invalid: this.checkInvalid(),
edited: this.isEdited(),
added: this.isNew(),
} as Annotations<T>
}
}
async createFirstEntryForList () {
const spec: ValueSpec = this.spec()
if (spec.type === 'object' && !this.config()) {
pointer.set(this.rootConfig, this.ptr, getDefaultObject(spec.spec))
}
if (spec.type === 'union' && !this.config()) {
pointer.set(this.rootConfig, this.ptr, getDefaultUnion(spec))
}
}
injectModalData (res: { data?: any }): void {
if (res.data !== undefined) {
pointer.set(this.rootConfig, this.ptr, res.data)
}
}
}
function isEqual (uniqueBy: UniqueBy, lhs: ConfigCursor<'object'>, rhs: ConfigCursor<'object'>): boolean {
if (uniqueBy === null) {
return false
} else if (typeof uniqueBy === 'string') {
return lhs.seekNext(uniqueBy).equals(rhs.seekNext(uniqueBy))
} else if ('any' in uniqueBy) {
for (let subSpec of uniqueBy.any) {
if (isEqual(subSpec, lhs, rhs)) {
return true
}
}
return false
} else if ('all' in uniqueBy) {
for (let subSpec of uniqueBy.all) {
if (!isEqual(subSpec, lhs, rhs)) {
return false
}
}
return true
}
}
export function displayUniqueBy (uniqueBy: UniqueBy, spec: ValueSpecObject | ValueSpecUnion, value: object): string {
if (typeof uniqueBy === 'string') {
if (spec.type === 'object') {
return spec.spec[uniqueBy].name
} else if (spec.type === 'union') {
if (uniqueBy === spec.tag.id) {
return spec.tag.name
} else {
return spec.variants[value[spec.tag.id]][uniqueBy].name
}
}
} else if ('any' in uniqueBy) {
return uniqueBy.any.map(uq => {
if (typeof uq === 'object' && 'all' in uq) {
return `(${displayUniqueBy(uq, spec, value)})`
} else {
return displayUniqueBy(uq, spec, value)
}
}).join(' and ')
} else if ('all' in uniqueBy) {
return uniqueBy.all.map(uq => {
if (typeof uq === 'object' && 'any' in uq) {
return `(${displayUniqueBy(uq, spec, value)})`
} else {
return displayUniqueBy(uq, spec, value)
}
}).join(' or ')
}
}