Be specific instead of any in Inquire and SchemaData

main
Mari 3 years ago
parent 68593b1cf0
commit d3d820e60a
  1. 12
      src/commands/AddEntry.ts
  2. 6
      src/prompts/Inquire.ts
  3. 2
      src/prompts/implementations/ConditionPrompt.ts
  4. 3
      src/prompts/implementations/EmpathyGroupListPrompt.ts
  5. 5
      src/prompts/implementations/EmpathyGroupPrompt.ts
  6. 6
      src/prompts/implementations/EmpathyGuidePrompt.ts
  7. 14
      src/prompts/implementations/EntryMainMenuPrompt.ts
  8. 4
      src/prompts/implementations/GuidedEmpathyListPrompt.ts
  9. 4
      src/prompts/implementations/GuidedEmpathyPrompt.ts
  10. 2
      src/prompts/implementations/JournalEntryPrompt.ts
  11. 2
      src/prompts/implementations/SuicidalityPrompt.ts
  12. 2
      src/prompts/implementations/SummaryPrompt.ts
  13. 33
      src/prompts/types/HierarchicalCheckboxInput.ts
  14. 6
      src/prompts/types/MultiTextInput.ts
  15. 21
      src/repository/LocalRepository.ts
  16. 46
      src/schemata/SchemaData.ts
  17. 15
      src/schemata/YAMLPrompt.ts
  18. 2
      src/utils/Merge.ts

@ -33,30 +33,41 @@ export function addEntryCommand(): CommandModule {
const empathyList = guidedEmpathyListPrompt({inquire, promptForEmpathy: empathy}) const empathyList = guidedEmpathyListPrompt({inquire, promptForEmpathy: empathy})
const mainMenuItems = [ const mainMenuItems = [
// TODO: add ability to amend previous entry
condition.mainMenu, condition.mainMenu,
summary.mainMenu, summary.mainMenu,
new Separator(), new Separator(),
journal.mainMenu, journal.mainMenu,
empathyList.mainMenu, empathyList.mainMenu,
// TODO: Modified HierarchicalCheckboxInput to allow for putting more things in the main menu than
// there are letters
// TODO: gratitude journaling
// TODO: thinking about the future - where do you want to be in x years type thing
// TODO: breathing regulation exercises
// TODO: Goals for the day and checking in on how yesterday's goals went
/* /*
{ {
name: typeof entry.needs === "object" ? "Check back in on needs" : "Check in on needs", name: typeof entry.needs === "object" ? "Check back in on needs" : "Check in on needs",
// TODO: physical needs: food, water, sleep, exercise tracking, possibly with autocompletes for each saved in a file like the empathy guide
value: NEEDS, value: NEEDS,
key: "n" key: "n"
}, },
{ {
name: typeof entry.personas === "object" ? "Check back in on personas" : "Check in on personas", name: typeof entry.personas === "object" ? "Check back in on personas" : "Check in on personas",
// TODO: Personas' individual journal entries
value: PERSONA, value: PERSONA,
key: "p" key: "p"
}, },
{name: typeof entry.rpg === "object" ? "Change RPG stats" : "Add RPG stats", value: RPG, key: "r"}, {name: typeof entry.rpg === "object" ? "Change RPG stats" : "Add RPG stats", value: RPG, key: "r"},
{ {
name: typeof entry.activities === "object" ? "Change record of recent activities" : "Add record of recent activities", name: typeof entry.activities === "object" ? "Change record of recent activities" : "Add record of recent activities",
// TODO: Add general activity set with historical activities saved in a file like the empathy guide
value: ACTIVITIES, value: ACTIVITIES,
key: "a" key: "a"
}, },
{ {
name: typeof entry.music === "object" ? "Change record of recently played music" : "Add record of recently played music", name: typeof entry.music === "object" ? "Change record of recently played music" : "Add record of recently played music",
// TODO: Add music
value: MUSIC, value: MUSIC,
key: "m" key: "m"
}, },
@ -65,6 +76,7 @@ export function addEntryCommand(): CommandModule {
/* /*
{ {
name: typeof entry.recoveries === "object" ? "Try more recovery methods" : "Try some recovery methods", name: typeof entry.recoveries === "object" ? "Try more recovery methods" : "Try some recovery methods",
// TODO: Add list of recovery methods
value: RECOVERIES, value: RECOVERIES,
key: "y" key: "y"
}, },

@ -1,9 +1,9 @@
import {prompt, QuestionCollection} from "inquirer"; import {prompt, QuestionMap} from "inquirer";
export type InquireFunction = (question: Omit<QuestionCollection, "name">) => Promise<any> export type InquireFunction<QuestionT extends QuestionMap[keyof QuestionMap], AnswerT extends QuestionT["default"]> = (question: QuestionT) => Promise<AnswerT>
export type ShowFunction = (text: string) => Promise<void> export type ShowFunction = (text: string) => Promise<void>
export async function inquire(question: Omit<QuestionCollection, "name">): Promise<any> { export async function inquire<QuestionT extends QuestionMap[keyof QuestionMap], AnswerT extends QuestionT["default"]>(question: QuestionT): Promise<AnswerT> {
const result = await prompt([{...question, name: "answer"}]) const result = await prompt([{...question, name: "answer"}])
return result.answer return result.answer
} }

@ -10,7 +10,7 @@ export interface ConditionPromptOptions extends Partial<Omit<ListQuestion, "name
} }
export interface ConditionPromptDependencies { export interface ConditionPromptDependencies {
readonly inquire: InquireFunction readonly inquire: InquireFunction<ListQuestion, Condition>
} }
export function conditionPrompt(deps: ConditionPromptDependencies): { export function conditionPrompt(deps: ConditionPromptDependencies): {

@ -4,6 +4,7 @@ import chalk from "chalk";
import pluralize from "pluralize"; import pluralize from "pluralize";
import {InquireFunction} from "../Inquire"; import {InquireFunction} from "../Inquire";
import {EmpathyGroupPromptOptions} from "./EmpathyGroupPrompt"; import {EmpathyGroupPromptOptions} from "./EmpathyGroupPrompt";
import {ListQuestion} from "inquirer";
export interface EmpathyGroupListPromptOptions { export interface EmpathyGroupListPromptOptions {
readonly default?: readonly EmpathyGroup[] readonly default?: readonly EmpathyGroup[]
@ -11,7 +12,7 @@ export interface EmpathyGroupListPromptOptions {
} }
export interface EmpathyGroupListPromptDependencies { export interface EmpathyGroupListPromptDependencies {
readonly inquire: InquireFunction readonly inquire: InquireFunction<ListQuestion, number>
readonly promptForEmpathyGroup: (opts: EmpathyGroupPromptOptions) => Promise<EmpathyGroup | null> readonly promptForEmpathyGroup: (opts: EmpathyGroupPromptOptions) => Promise<EmpathyGroup | null>
} }

@ -1,8 +1,9 @@
import capitalize from "capitalize"; import capitalize from "capitalize";
import {editYaml} from "../../schemata/YAMLPrompt"; import {editYaml, EditYamlInquire} from "../../schemata/YAMLPrompt";
import chalk from "chalk"; import chalk from "chalk";
import {InquireFunction, ShowFunction} from "../Inquire"; import {InquireFunction, ShowFunction} from "../Inquire";
import {EmpathyGroup, EmpathyGroupJTD} from "../../datatypes/EmpathyGroup"; import {EmpathyGroup, EmpathyGroupJTD} from "../../datatypes/EmpathyGroup";
import {ExpandQuestion, InputQuestion, MultiTextInputQuestion} from "inquirer";
export interface EmpathyGroupPromptOptions { export interface EmpathyGroupPromptOptions {
default?: EmpathyGroup default?: EmpathyGroup
@ -10,7 +11,7 @@ export interface EmpathyGroupPromptOptions {
} }
export interface EmpathyGroupPromptDependencies { export interface EmpathyGroupPromptDependencies {
inquire: InquireFunction inquire: InquireFunction<ExpandQuestion, typeof PROMPT|typeof AUTO|typeof YAML> & EditYamlInquire & InquireFunction<InputQuestion, string> & InquireFunction<MultiTextInputQuestion, string[]>
showError: ShowFunction showError: ShowFunction
} }

@ -2,8 +2,8 @@ import {isPopulatedArray} from "../../utils/Arrays";
import chalk from "chalk"; import chalk from "chalk";
import pluralize from "pluralize"; import pluralize from "pluralize";
import {totalItems} from "../../datatypes/EmpathyGroupList"; import {totalItems} from "../../datatypes/EmpathyGroupList";
import {Separator} from "inquirer"; import {ExpandQuestion, Separator} from "inquirer";
import {editYaml} from "../../schemata/YAMLPrompt"; import {editYaml, EditYamlInquire} from "../../schemata/YAMLPrompt";
import {InquireFunction, ShowFunction} from "../Inquire"; import {InquireFunction, ShowFunction} from "../Inquire";
import {EmpathyGroupListPromptOptions} from "./EmpathyGroupListPrompt"; import {EmpathyGroupListPromptOptions} from "./EmpathyGroupListPrompt";
import {EmpathyGroup} from "../../datatypes/EmpathyGroup"; import {EmpathyGroup} from "../../datatypes/EmpathyGroup";
@ -14,7 +14,7 @@ export interface EmpathyGuidePromptOptions {
} }
export interface EmpathyGuidePromptDependencies { export interface EmpathyGuidePromptDependencies {
readonly inquire: InquireFunction readonly inquire: InquireFunction<ExpandQuestion, typeof PLEASANT | typeof UNPLEASANT | typeof NEEDS | typeof INSPECT | typeof SAVE> & EditYamlInquire
readonly showError: ShowFunction readonly showError: ShowFunction
readonly promptForEmpathyGroupList: (opts: EmpathyGroupListPromptOptions) => Promise<readonly EmpathyGroup[]> readonly promptForEmpathyGroupList: (opts: EmpathyGroupListPromptOptions) => Promise<readonly EmpathyGroup[]>
} }

@ -1,13 +1,15 @@
import {InquireFunction, ShowFunction} from "../Inquire"; import {InquireFunction, ShowFunction} from "../Inquire";
import {ExpandChoiceOptions, Separator, SeparatorOptions} from "inquirer"; import {ExpandChoiceOptions, ExpandQuestion, Separator, SeparatorOptions} from "inquirer";
import {merge} from "../../utils/Merge"; import {merge} from "../../utils/Merge";
import {Entry, EntryJTD} from "../../datatypes/Entry"; import {Entry, EntryJTD} from "../../datatypes/Entry";
import {editYaml} from "../../schemata/YAMLPrompt"; import {editYaml, EditYamlInquire} from "../../schemata/YAMLPrompt";
const VIEW = ".View" const VIEW = ".View"
const DONE = ".Done" const DONE = ".Done"
export interface EntryMainMenuChoice<PropertyT extends string & keyof EntryMainMenuOptions> { type EntryMainMenuChoiceKey = string & keyof EntryMainMenuOptions
export interface EntryMainMenuChoice<PropertyT extends EntryMainMenuChoiceKey> {
readonly type: "mainMenu" readonly type: "mainMenu"
readonly property: PropertyT readonly property: PropertyT
choice(input: EntryMainMenuOptions[PropertyT]): ExpandChoiceOptions & { value: PropertyT } choice(input: EntryMainMenuOptions[PropertyT]): ExpandChoiceOptions & { value: PropertyT }
@ -57,8 +59,8 @@ export function makeEntryMainMenuChoice<PropertyT extends string & keyof EntryMa
export interface EntryMainMenuOptions extends Omit<Entry, "finishedAt"> {} export interface EntryMainMenuOptions extends Omit<Entry, "finishedAt"> {}
export interface EntryMainMenuDependencies { export interface EntryMainMenuDependencies {
readonly inquire: InquireFunction readonly inquire: InquireFunction<ExpandQuestion, typeof DONE | typeof VIEW | EntryMainMenuChoiceKey> & EditYamlInquire
readonly choices: readonly (EntryMainMenuChoice<any>|SeparatorOptions)[] readonly choices: readonly (EntryMainMenuChoice<EntryMainMenuChoiceKey>|SeparatorOptions)[]
readonly showError: ShowFunction readonly showError: ShowFunction
} }
@ -106,7 +108,7 @@ export async function promptForEntryMainMenu(entry: EntryMainMenuOptions, deps:
const withoutFinishedAt = {...updated, finishedAt: undefined} const withoutFinishedAt = {...updated, finishedAt: undefined}
return continueWith(withoutFinishedAt) return continueWith(withoutFinishedAt)
} else { } else {
const selectedChoice = choices.find((choice) => choice.type === "mainMenu" && choice.property === result) as EntryMainMenuChoice<any> const selectedChoice = choices.find((choice) => choice.type === "mainMenu" && choice.property === result) as EntryMainMenuChoice<string & keyof EntryMainMenuOptions>
return continueWith(await onSelected(selectedChoice, entry)) return continueWith(await onSelected(selectedChoice, entry))
} }
} }

@ -5,7 +5,7 @@ import {EntryMainMenuChoice, makeEntryMainMenuChoice} from "./EntryMainMenuPromp
import chalk from "chalk"; import chalk from "chalk";
import pluralize from "pluralize"; import pluralize from "pluralize";
import {isPopulatedArray} from "../../utils/Arrays"; import {isPopulatedArray} from "../../utils/Arrays";
import {ListChoiceOptions} from "inquirer"; import {ListChoiceOptions, ListQuestion} from "inquirer";
import {asDefault} from "../../utils/Objects"; import {asDefault} from "../../utils/Objects";
import {GuidedEmpathyPromptOptions} from "./GuidedEmpathyPrompt"; import {GuidedEmpathyPromptOptions} from "./GuidedEmpathyPrompt";
@ -14,7 +14,7 @@ export interface GuidedEmpathyListPromptOptions extends PersonaPrompt {
} }
export interface GuidedEmpathyListPromptDependencies { export interface GuidedEmpathyListPromptDependencies {
readonly inquire: InquireFunction readonly inquire: InquireFunction<ListQuestion, number>
readonly promptForEmpathy: (input: GuidedEmpathyPromptOptions) => Promise<GuidedEmpathy|null> readonly promptForEmpathy: (input: GuidedEmpathyPromptOptions) => Promise<GuidedEmpathy|null>
} }

@ -11,14 +11,14 @@ import {isPopulatedArray} from "../../utils/Arrays";
import Separator from "inquirer/lib/objects/separator"; import Separator from "inquirer/lib/objects/separator";
import {EmpathyGroup} from "../../datatypes/EmpathyGroup"; import {EmpathyGroup} from "../../datatypes/EmpathyGroup";
import {GuidedEmpathy, guidedEmpathyToString, isPopulatedGuidedEmpathy} from "../../datatypes/GuidedEmpathy"; import {GuidedEmpathy, guidedEmpathyToString, isPopulatedGuidedEmpathy} from "../../datatypes/GuidedEmpathy";
import {HierarchicalCheckboxChildChoice, HierarchicalCheckboxParentChoice } from "inquirer"; import {HierarchicalCheckboxChildChoice, HierarchicalCheckboxParentChoice, HierarchicalCheckboxQuestion, MultiTextInputQuestion } from "inquirer";
export interface GuidedEmpathyPromptOptions extends PersonaPrompt { export interface GuidedEmpathyPromptOptions extends PersonaPrompt {
readonly default?: GuidedEmpathy readonly default?: GuidedEmpathy
} }
export interface GuidedEmpathyPromptDependencies { export interface GuidedEmpathyPromptDependencies {
readonly inquire: InquireFunction, readonly inquire: InquireFunction<HierarchicalCheckboxQuestion, readonly string[]> & InquireFunction<MultiTextInputQuestion, readonly string[]>
readonly guideFactory: () => EmpathyGuide, readonly guideFactory: () => EmpathyGuide,
readonly show: ShowFunction, readonly show: ShowFunction,
} }

@ -10,7 +10,7 @@ import {asDefault} from "../../utils/Objects";
export interface JournalEntryPromptOptions extends Partial<Omit<EditorQuestion, "name" | "type">>, PersonaPrompt {} export interface JournalEntryPromptOptions extends Partial<Omit<EditorQuestion, "name" | "type">>, PersonaPrompt {}
export interface JournalEntryPromptDependencies { export interface JournalEntryPromptDependencies {
readonly inquire: InquireFunction readonly inquire: InquireFunction<EditorQuestion, string>
} }
export function journalEntryPrompt(deps: JournalEntryPromptDependencies): { export function journalEntryPrompt(deps: JournalEntryPromptDependencies): {

@ -9,7 +9,7 @@ export interface SuicidalityPromptOptions extends Partial<Omit<ListQuestion, "na
} }
export interface SuicidalityPromptDependencies { export interface SuicidalityPromptDependencies {
inquire: InquireFunction inquire: InquireFunction<ListQuestion, Suicidality>
} }
export function suicidalityPrompt(deps: SuicidalityPromptDependencies): { export function suicidalityPrompt(deps: SuicidalityPromptDependencies): {

@ -10,7 +10,7 @@ import {asDefault} from "../../utils/Objects";
export interface SummaryPromptOptions extends Partial<Omit<InputQuestion, "name" | "type">>, PersonaPrompt {} export interface SummaryPromptOptions extends Partial<Omit<InputQuestion, "name" | "type">>, PersonaPrompt {}
export interface SummaryPromptDependencies { export interface SummaryPromptDependencies {
readonly inquire: InquireFunction readonly inquire: InquireFunction<InputQuestion, string>
} }
export function summaryPrompt(deps: SummaryPromptDependencies): { export function summaryPrompt(deps: SummaryPromptDependencies): {

@ -17,6 +17,7 @@ import figures from "figures";
import Paginator from "inquirer/lib/utils/paginator"; import Paginator from "inquirer/lib/utils/paginator";
import {isPopulatedArray} from "../../utils/Arrays"; import {isPopulatedArray} from "../../utils/Arrays";
import {filter as fuzzyFilter, FilterOptions} from "fuzzy"; import {filter as fuzzyFilter, FilterOptions} from "fuzzy";
import ScreenManager from "inquirer/lib/utils/screen-manager";
interface ExtendedReadLine extends ReadLineInterface { interface ExtendedReadLine extends ReadLineInterface {
line: string line: string
@ -51,7 +52,7 @@ declare module "inquirer" {
export interface HierarchicalCheckboxQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, HierarchicalCheckboxQuestionOptions { export interface HierarchicalCheckboxQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, HierarchicalCheckboxQuestionOptions {
/** @inheritDoc */ /** @inheritDoc */
type: "hierarchical-checkbox" type: "hierarchical-checkbox"
default?: string[] default?: readonly string[]
} }
export interface QuestionMap { export interface QuestionMap {
@ -59,6 +60,14 @@ declare module "inquirer" {
} }
} }
interface ExtendedPaginatorConstructor {
new(manager: ScreenManager, options: {isInfinite:boolean}): ExtendedPaginator
}
interface ExtendedPaginator extends Paginator {
paginate(content: string, selectedIndex: number, pageSize?: number): string;
}
interface NormalizedChoiceBase { interface NormalizedChoiceBase {
readonly name: string readonly name: string
readonly value: string|null readonly value: string|null
@ -260,14 +269,17 @@ export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, Questi
this.updateUnlistedValues() this.updateUnlistedValues()
} }
// @ts-ignore _run(callback: Exclude<HierarchicalCheckboxInput["done"], null>, error?: (err: Error) => void) {
_run(callback: HierarchicalCheckboxInput["done"], error: (err: Error) => void) {
const invalidChoices = Object.keys(this.invalidValues); const invalidChoices = Object.keys(this.invalidValues);
const invalidValues = this.value.filter((value) => !this.valueMap.hasOwnProperty(value)); const invalidValues = this.value.filter((value) => !this.valueMap.hasOwnProperty(value));
if (isPopulatedArray(invalidChoices)) { if (isPopulatedArray(invalidChoices)) {
error(Error(`Duplicate values: ${invalidChoices.join()}`)) if (error) {
error(Error(`Duplicate values: ${invalidChoices.join()}`))
}
} else if (isPopulatedArray(invalidValues)) { } else if (isPopulatedArray(invalidValues)) {
error(Error(`Unknown values: ${invalidValues.join()}`)) if (error) {
error(Error(`Unknown values: ${invalidValues.join()}`))
}
} }
this.rl.on("history", (history) => { this.rl.on("history", (history) => {
history.splice(0, history.length) history.splice(0, history.length)
@ -294,13 +306,12 @@ export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, Questi
private readonly valueMap: ValidValueMap private readonly valueMap: ValidValueMap
private readonly invalidValues: InvalidValueMap private readonly invalidValues: InvalidValueMap
private readonly value: string[] private readonly value: string[]
private done: (state: any) => void|null private done: ((state: unknown) => void)|null
private location: Breadcrumb private location: Breadcrumb
private activeIndex: number private activeIndex: number
private unlistedValues: string[] private unlistedValues: string[]
private submitter: Subject<string[]> = new Subject<string[]>() private submitter: Subject<string[]> = new Subject<string[]>()
// @ts-ignore private paginator: ExtendedPaginator = new (Paginator as ExtendedPaginatorConstructor)(this.screen, {isInfinite: true});
private paginator: Paginator = new Paginator(this.screen, {isInfinite: true});
private scheduledRender: NodeJS.Immediate|null = null private scheduledRender: NodeJS.Immediate|null = null
private get readline(): ExtendedReadLine { private get readline(): ExtendedReadLine {
@ -563,7 +574,7 @@ export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, Questi
children: this.readline.line === "" ? this.location.searchable : fuzzyFilter(this.readline.line, this.location.searchable.slice(), { children: this.readline.line === "" ? this.location.searchable : fuzzyFilter(this.readline.line, this.location.searchable.slice(), {
pre: searchHighlightPrefix, pre: searchHighlightPrefix,
post: searchHighlightSuffix, post: searchHighlightSuffix,
extract(input: any): string { extract(input: NormalizedLeafChoice | NormalizedParentChoice): string {
return input.name return input.name
} }
} as FilterOptions<NormalizedParentChoice | NormalizedLeafChoice>).map((result) => ({ } as FilterOptions<NormalizedParentChoice | NormalizedLeafChoice>).map((result) => ({
@ -644,7 +655,9 @@ export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, Questi
this.status = "answered" this.status = "answered"
this.render() this.render()
this.screen.done() this.screen.done()
this.done(value) if (this.done !== null) {
this.done(value)
}
} }
private onOtherKey() { private onOtherKey() {

@ -21,11 +21,11 @@ declare module "inquirer" {
export interface MultiTextInputQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, MultiTextInputQuestionOptions { export interface MultiTextInputQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, MultiTextInputQuestionOptions {
/** @inheritDoc */ /** @inheritDoc */
type: "multitext" type: "multitext"
default?: string[] default?: readonly string[]
} }
export interface QuestionMap { export interface QuestionMap {
["multitext"]: HierarchicalCheckboxQuestion ["multitext"]: MultiTextInputQuestion
} }
} }
@ -60,7 +60,7 @@ export class MultiTextInput<AnswerT extends Answers = Answers, QuestionT extends
this.scheduleRender() this.scheduleRender()
} }
private done: (state: any) => void|null private done: (state: unknown) => void|null
private activeIndex: number private activeIndex: number
private readonly value: string[] private readonly value: string[]
private submitter: Subject<string[]> = new Subject<string[]>() private submitter: Subject<string[]> = new Subject<string[]>()

@ -5,7 +5,7 @@ import {
ReferencedTypes, ReferencedTypes,
schema, schema,
SchemaData, SchemaData,
UntypedReferenceList AnyReferenceList, Value
} from "../schemata/SchemaData"; } from "../schemata/SchemaData";
import {rm, open, FileHandle} from "fs/promises"; import {rm, open, FileHandle} from "fs/promises";
import {join, dirname} from "path"; import {join, dirname} from "path";
@ -25,6 +25,12 @@ export const AutosaveFileJTD = schema({
references: [] references: []
}) })
export interface LocalRepositoryDependencies {
rm: typeof rm
open: typeof open
makeDir: typeof makeDir
}
export class LocalRepository { export class LocalRepository {
private readonly paths: envPaths.Paths private readonly paths: envPaths.Paths
@ -120,10 +126,11 @@ export class LocalRepository {
} }
} }
async loadAutosaveObjects<TypesT extends UntypedReferenceList>( // TODO: Enable autosave for all commands
async loadAutosaveObjects<TypesT extends AnyReferenceList>(
{autosaveNamespace, schemas}: { autosaveNamespace: string, schemas: TypesT }): Promise<{ {autosaveNamespace, schemas}: { autosaveNamespace: string, schemas: TypesT }): Promise<{
validated: Partial<ReferencedTypes<TypesT>>, validated: Partial<ReferencedTypes<TypesT>>,
unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, unknown>> unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, string>>
}> { }> {
const path = this.getAutosaveFilePath(autosaveNamespace) const path = this.getAutosaveFilePath(autosaveNamespace)
let handle: FileHandle let handle: FileHandle
@ -143,14 +150,14 @@ export class LocalRepository {
throw Error(`Autosave file ${autosaveNamespace} was too corrupted to make sense of`) throw Error(`Autosave file ${autosaveNamespace} was too corrupted to make sense of`)
} }
const validated: Partial<ReferencedTypes<TypesT>> = {} const validated: Partial<ReferencedTypes<TypesT>> = {}
const unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, unknown>> = {} const unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, string>> = {}
schemas.forEach((schema: ReferencedSchema<TypesT>) => { schemas.forEach((schema: ReferencedSchema<TypesT>) => {
if (autosaved.hasOwnProperty(schema.key)) { if (autosaved.hasOwnProperty(schema.key)) {
const value = autosaved[schema.key] const value = autosaved[schema.key]
if (!schema.validate(value)) { if (!schema.validate(value)) {
unvalidated[schema.key as keyof ReferencedTypes<TypesT>] = value unvalidated[schema.key as keyof ReferencedTypes<TypesT>] = value
} else { } else {
validated[schema.key as keyof ReferencedTypes<TypesT>] = value validated[schema.key as keyof ReferencedTypes<TypesT>] = value as Value<ReferencedSchema<TypesT>>
} }
} }
}) })
@ -160,7 +167,7 @@ export class LocalRepository {
} }
} }
async saveAutosaveObject<T>({schema, value, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<T, any, any, any>, value: T}): Promise<void> { async saveAutosaveObject<T>({schema, value, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<T, string, AnyReferenceList, ReferencedTypes<AnyReferenceList>>, value: T}): Promise<void> {
const path = this.getAutosaveFilePath(autosaveNamespace) const path = this.getAutosaveFilePath(autosaveNamespace)
await makeDir(dirname(path)) await makeDir(dirname(path))
const handle = await open(path, "a+") const handle = await open(path, "a+")
@ -178,7 +185,7 @@ export class LocalRepository {
} }
} }
async clearAutosaveObject({schema, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<any, any, any, any>}): Promise<void> { async clearAutosaveObject({schema, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<unknown, string, AnyReferenceList, ReferencedTypes<AnyReferenceList>>}): Promise<void> {
const path = this.getAutosaveFilePath(autosaveNamespace) const path = this.getAutosaveFilePath(autosaveNamespace)
await makeDir(dirname(path)) await makeDir(dirname(path))
const handle = await open(path, "a+") const handle = await open(path, "a+")

@ -3,7 +3,18 @@ import AJV, {ValidateFunction, JTDSchemaType} from "ajv/dist/jtd";
const ajv = new AJV(); const ajv = new AJV();
export type SchemaData<RepresentedT, ReferenceT extends string, ReferencesT extends UntypedReferenceList, DefinitionsT extends ReferencedTypes<ReferencesT>> = { interface BaseSchemaData<RepresentedT, ReferenceT extends string> {
value?: RepresentedT,
schema: unknown,
key: ReferenceT,
definition: { [key in ReferenceT]: unknown }
referenced?: { [key in ReferenceT]: RepresentedT }
reference: { "ref": ReferenceT },
validate: ValidateFunction<RepresentedT>
requiredReferences: AnyReferenceList
}
export interface SchemaData<RepresentedT, ReferenceT extends string, ReferencesT extends AnyReferenceList, DefinitionsT extends ReferencedTypes<ReferencesT>> extends BaseSchemaData<RepresentedT, ReferenceT> {
value?: RepresentedT, value?: RepresentedT,
schema: JTDSchemaType<RepresentedT, DefinitionsT>, schema: JTDSchemaType<RepresentedT, DefinitionsT>,
key: ReferenceT, key: ReferenceT,
@ -14,35 +25,38 @@ export type SchemaData<RepresentedT, ReferenceT extends string, ReferencesT exte
requiredReferences: ReferencesT requiredReferences: ReferencesT
} }
export type UntypedReferenceList = SchemaData<any, any, any, any>[] export type AnySchemaData = BaseSchemaData<unknown, string>
export type AnySchemaDataFor<RepresentedT> = BaseSchemaData<RepresentedT, string>
export type AnyReferenceList = AnySchemaData[]
export type AnyDefinitions = Record<string, unknown>
export type Schema<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["schema"], undefined> export type Schema<DataT extends AnySchemaData> = Exclude<DataT["schema"], undefined>
export type Value<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["value"], undefined> export type Value<DataT extends AnySchemaData> = Exclude<DataT["value"], undefined>
export type Definition<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["definition"], undefined> export type Definition<DataT extends AnySchemaData> = Exclude<DataT["definition"], undefined>
export type Reference<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["reference"], undefined> export type Reference<DataT extends AnySchemaData> = Exclude<DataT["reference"], undefined>
export type ReferenceKey<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["key"], undefined> export type ReferenceKey<DataT extends AnySchemaData> = Exclude<DataT["key"], undefined>
export type Referenced<DataT extends SchemaData<any, any, any, any>> = Exclude<DataT["referenced"], undefined> export type Referenced<DataT extends AnySchemaData> = Exclude<DataT["referenced"], undefined>
export type ReferencedSchemaMap<ReferencesT extends UntypedReferenceList> = { export type ReferencedSchemaMap<ReferencesT extends AnyReferenceList> = {
[Property in keyof ReferencesT as ReferencesT[Property] extends SchemaData<any, any, any, any> ? ReferenceKey<ReferencesT[Property]> : never]: ReferencesT[Property] extends SchemaData<any, any, any, any> ? ReferencesT[Property] : never [Property in keyof ReferencesT as ReferencesT[Property] extends AnySchemaData ? ReferenceKey<ReferencesT[Property]> : never]: ReferencesT[Property] extends AnySchemaData ? ReferencesT[Property] : never
} }
export type ReferencedSchema<ReferencesT extends UntypedReferenceList> = ReferencedSchemaMap<ReferencesT>[keyof ReferencedSchemaMap<ReferencesT>] export type ReferencedSchema<ReferencesT extends AnyReferenceList> = ReferencedSchemaMap<ReferencesT>[keyof ReferencedSchemaMap<ReferencesT>]
export type ReferencedDefinitions<ReferencesT extends UntypedReferenceList> = { export type ReferencedDefinitions<ReferencesT extends AnyReferenceList> = {
[Property in keyof ReferencedSchemaMap<ReferencesT>]?: Definition<ReferencedSchemaMap<ReferencesT>[Property]> [Property in keyof ReferencedSchemaMap<ReferencesT>]?: Definition<ReferencedSchemaMap<ReferencesT>[Property]>
} }
export type ReferencedTypes<ReferencesT extends UntypedReferenceList> = { export type ReferencedTypes<ReferencesT extends AnyReferenceList> = {
[Property in keyof ReferencedSchemaMap<ReferencesT>]?: Value<ReferencedSchemaMap<ReferencesT>[Property]> [Property in keyof ReferencedSchemaMap<ReferencesT>]?: Value<ReferencedSchemaMap<ReferencesT>[Property]>
} }
export function schema< export function schema<
RepresentedT, RepresentedT,
KeyT extends string, KeyT extends string,
ReferencesT extends SchemaData<any, any, any, ReferencedTypes<ReferencesT>>[], ReferencesT extends AnySchemaData[],
>({schema, key, references}: { schema: JTDSchemaType<RepresentedT, ReferencedTypes<ReferencesT>>, key: KeyT, references: ReferencesT, typeHint?: RepresentedT|null }): SchemaData<RepresentedT, KeyT, ReferencesT, ReferencedTypes<ReferencesT>> { >({schema, key, references}: { schema: JTDSchemaType<RepresentedT, ReferencedTypes<ReferencesT>>, key: KeyT, references: ReferencesT, typeHint?: RepresentedT|null }): SchemaData<RepresentedT, KeyT, ReferencesT, ReferencedTypes<ReferencesT>> {
const definition = {[key]: schema} as Definition<SchemaData<RepresentedT, KeyT, ReferencesT, ReferencedTypes<ReferencesT>>> const definition = {[key]: schema} as Definition<SchemaData<RepresentedT, KeyT, ReferencesT, ReferencedTypes<ReferencesT>>>
const reference = {ref: key} const reference = {ref: key}
const definitions = references.reduce((definitions, reference) => ({...reference.definition, ...definitions}), {} as ReferencedDefinitions<ReferencesT>) const definitions = references.reduce<AnyDefinitions>((definitions, reference) => ({...reference.definition, ...definitions}), definition)
const validate = ajv.compile<RepresentedT>({ ...schema, definitions }) const validate = ajv.compile<RepresentedT>({ ...reference, definitions })
return { return {
schema, schema,
key, key,

@ -1,14 +1,17 @@
import {SchemaData} from "./SchemaData"; import {AnySchemaDataFor} from "./SchemaData";
import {dump, load} from "js-yaml"; import {dump, load} from "js-yaml";
import {EditorQuestionOptions} from "inquirer";
import {InquireFunction, ShowFunction} from "../prompts/Inquire"; import {InquireFunction, ShowFunction} from "../prompts/Inquire";
import {EditorQuestion, ExpandQuestion} from "inquirer";
const RETRY = "Retry" const RETRY = "Retry"
const ABORT = "Abort" const ABORT = "Abort"
export async function editYaml<ObjectT>({schema, currentValue, name, inquire, showError}: {schema: SchemaData<ObjectT, any, any, any>, currentValue: ObjectT, name: string, inquire: InquireFunction, showError: ShowFunction}): Promise<ObjectT> export type EditYamlInquire = InquireFunction<EditorQuestion, string> & InquireFunction<ExpandQuestion, typeof RETRY|typeof ABORT>
export async function editYaml<ObjectT>({schema, currentValue, name, inquire, showError}: {schema: SchemaData<ObjectT, any, any, any>, currentValue: ObjectT|undefined, name: string, inquire: InquireFunction, showError: ShowFunction}): Promise<ObjectT|undefined>
export async function editYaml<ObjectT>({schema, currentValue, name, inquire, showError}: {schema: SchemaData<ObjectT, any, any, any>, currentValue: ObjectT|undefined, name: string, inquire: InquireFunction, showError: ShowFunction}): Promise<ObjectT|undefined> { // TODO: Inject this one's dependencies (inquire, showError) as well, instead of making callers do it, and give it to said callers.
export async function editYaml<ObjectT extends object>({schema, currentValue, name, inquire, showError}: {schema: AnySchemaDataFor<ObjectT>, currentValue: ObjectT, name: string, inquire: EditYamlInquire, showError: ShowFunction}): Promise<ObjectT>
export async function editYaml<ObjectT extends object>({schema, currentValue, name, inquire, showError}: {schema: AnySchemaDataFor<ObjectT>, currentValue: ObjectT|undefined, name: string, inquire: EditYamlInquire, showError: ShowFunction}): Promise<ObjectT|undefined>
export async function editYaml<ObjectT extends object>({schema, currentValue, name, inquire, showError}: {schema: AnySchemaDataFor<ObjectT>, currentValue: ObjectT|undefined, name: string, inquire: EditYamlInquire, showError: ShowFunction}): Promise<ObjectT|undefined> {
const original = dump(currentValue) const original = dump(currentValue)
let text = original let text = original
while (true) { while (true) {
@ -16,7 +19,7 @@ export async function editYaml<ObjectT>({schema, currentValue, name, inquire, sh
type: "editor", type: "editor",
message: `View and edit ${name}:`, message: `View and edit ${name}:`,
default: text, default: text,
} as EditorQuestionOptions) })
if (original !== modified) { if (original !== modified) {
try { try {
const result = load(modified) const result = load(modified)

@ -1,4 +1,4 @@
export function merge<Update extends {readonly [key: string]: any}, Data extends Update>(update: Update, base: Data): Data { export function merge<Update extends {readonly [key: string]: unknown}, Data extends Update>(update: Update, base: Data): Data {
const data = { const data = {
...update, ...update,
...base, ...base,

Loading…
Cancel
Save