diff --git a/package-lock.json b/package-lock.json index 63bea503..f7ea842a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", + "@unsplash/ts-namespace-import-plugin": "^1.0.0", "builtin-modules": "3.3.0", "esbuild": "0.17.3", "esbuild-svelte": "^0.8.0", @@ -2073,6 +2074,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@unsplash/ts-namespace-import-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@unsplash/ts-namespace-import-plugin/-/ts-namespace-import-plugin-1.0.0.tgz", + "integrity": "sha512-Z/fuSAWte/OP1ctpM/8PtecGeBN358yR1rxMiVlTQ3xIvTTFNpjQuZuqkY3XfvWFQ8mvpqumQvS7hG8XnUkTWg==", + "dev": true + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", diff --git a/package.json b/package.json index 8e63e44e..08624fb9 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", + "@unsplash/ts-namespace-import-plugin": "^1.0.0", "builtin-modules": "3.3.0", "esbuild": "0.17.3", "esbuild-svelte": "^0.8.0", diff --git a/src/core/FormResult.ts b/src/core/FormResult.ts index 69dc55b0..7cb4d54b 100644 --- a/src/core/FormResult.ts +++ b/src/core/FormResult.ts @@ -1,8 +1,7 @@ +import { objectSelect } from './objectSelect'; import { stringifyYaml } from "obsidian"; import { log_error } from "../utils/Log"; import { ModalFormError } from "../utils/Error"; -import { object, optional, array, string, coerce } from "valibot"; -import { parse } from "@std"; type ResultStatus = "ok" | "cancelled"; @@ -17,16 +16,6 @@ function isPrimitiveArray(value: unknown): value is string[] { return Array.isArray(value) && value.every(isPrimitive) } -const KeysSchema = array(coerce(string(), String)) - -const PickOmitSchema = object({ - pick: optional(KeysSchema), - omit: optional(KeysSchema), -}); - -console.log(parse(PickOmitSchema, { pick: ['a', 'b'] })) -console.log(parse(PickOmitSchema, { pick: [11], omit: ['a', 'b'] })) -console.log(parse(PickOmitSchema, undefined)) export function formDataFromFormOptions(values: Record) { const result: ModalFormData = {}; @@ -48,15 +37,30 @@ export function formDataFromFormOptions(values: Record) { export default class FormResult { constructor(private data: ModalFormData, public status: ResultStatus) { } - asFrontmatterString() { - return stringifyYaml(this.data); + /** + * Transform the current data into a frontmatter string, which is expected + * to be enclosed in `---` when used in a markdown file. + * This method does not add the enclosing `---` to the string, + * so you can put it anywhere inside the frontmatter. + * @param {Object} [options] an options object describing what options to pick or omit + * @param {string[]} [options.pick] an array of key names to pick from the data + * @param {string[]} [options.omit] an array of key names to omit from the data + * @returns the data formatted as a frontmatter string + */ + asFrontmatterString(options?: unknown) { + const data = objectSelect(this.data, options) + return stringifyYaml(data); } /** * Return the current data as a block of dataview properties + * @param {Object} [options] an options object describing what options to pick or omit + * @param {string[]} [options.pick] an array of key names to pick from the data + * @param {string[]} [options.omit] an array of key names to omit from the data * @returns string */ - asDataviewProperties(): string { - return Object.entries(this.data) + asDataviewProperties(options?: unknown): string { + const data = objectSelect(this.data, options) + return Object.entries(data) .map(([key, value]) => `${key}:: ${Array.isArray(value) ? value.map((v) => JSON.stringify(v)) : value}` ) diff --git a/src/core/objectSelect.test.ts b/src/core/objectSelect.test.ts new file mode 100644 index 00000000..98acdb21 --- /dev/null +++ b/src/core/objectSelect.test.ts @@ -0,0 +1,59 @@ +import { objectSelect } from "./objectSelect"; + +describe("objectSelect", () => { + const obj = { + name: "John Doe", + age: 30, + hobbies: ["reading", "swimming"], + isEmployed: true, + }; + + it("should return the original object if no options are provided", () => { + expect(objectSelect(obj, {})).toEqual(obj); + }); + + it("should pick the specified properties from the object", () => { + const opts = { pick: ["name", "age"] }; + const expectedOutput = { name: "John Doe", age: 30 }; + expect(objectSelect(obj, opts)).toEqual(expectedOutput); + }); + + it("should omit the specified properties from the object", () => { + const opts = { omit: ["name", "hobbies"] }; + const expectedOutput = { age: 30, isEmployed: true }; + expect(objectSelect(obj, opts)).toEqual(expectedOutput); + }); + + it("should pick and omit properties from the object", () => { + const opts = { pick: ["name", "age"], omit: ["age"] }; + const expectedOutput = { name: "John Doe" }; + expect(objectSelect(obj, opts)).toEqual(expectedOutput); + }); + + it("should return the original object if the pick array is empty", () => { + const opts = { pick: [] }; + expect(objectSelect(obj, opts)).toEqual(obj); + }); + + it("should return an empty object if the omit array contains all properties", () => { + const opts = { omit: ["name", "age", "hobbies", "isEmployed"] }; + expect(objectSelect(obj, opts)).toEqual({}); + }); + + it("should return the original object if the omit array is empty", () => { + const opts = { omit: [] }; + expect(objectSelect(obj, opts)).toEqual(obj); + }); + + it("should return the original object if the options argument is not an object", () => { + expect(objectSelect(obj, "invalid options")).toEqual(obj); + }); + + it("should return the original object if the options argument is null", () => { + expect(objectSelect(obj, null)).toEqual(obj); + }); + + it("should return the original object if the options argument is undefined", () => { + expect(objectSelect(obj, undefined)).toEqual(obj); + }); +}); diff --git a/src/core/objectSelect.ts b/src/core/objectSelect.ts new file mode 100644 index 00000000..9adb0c48 --- /dev/null +++ b/src/core/objectSelect.ts @@ -0,0 +1,46 @@ +import { E, parse, pipe } from "@std"; +import * as O from "fp-ts/Option"; +import * as NEA from "fp-ts/NonEmptyArray"; +import { filterWithIndex } from "fp-ts/lib/Record"; +import { object, optional, array, string, coerce } from "valibot"; + +const KeysSchema = array(coerce(string(), String)) + +const PickOmitSchema = object({ + pick: optional(KeysSchema), + omit: optional(KeysSchema), +}); + + + +/** + * Utility to pick/omit keys from an object. + * It is user facing, that's why we are so defensive in the options. + * The object should come from the form results, so we don't need to be that strict. + * @param obj the object you want to pick/omit from + * @param opts the options for picking/omitting based on key names + */ +export function objectSelect(obj: Record, opts: unknown): Record { + return pipe( + parse(PickOmitSchema, opts, { abortEarly: true }), + E.map((opts) => { + const picked = pipe( + O.fromNullable(opts.pick), + O.flatMap(NEA.fromArray), + O.map((pick) => { + return filterWithIndex((k) => pick.includes(k))(obj); + }), + O.getOrElse(() => obj) + ); + return pipe( + O.fromNullable(opts.omit), + O.map((omit) => + filterWithIndex((k) => !omit.includes(k))(picked)), + O.getOrElse(() => picked) + ) + } + ), + E.getOrElse(() => obj) + ) +} + diff --git a/src/std/index.ts b/src/std/index.ts index c01cb50a..2d2a2cbf 100644 --- a/src/std/index.ts +++ b/src/std/index.ts @@ -1,6 +1,6 @@ import { pipe as p } from "fp-ts/function"; import { partitionMap } from "fp-ts/Array"; -import { isLeft, isRight, tryCatchK } from "fp-ts/Either"; +import { isLeft, isRight, tryCatchK, map, getOrElse } from "fp-ts/Either"; import { ValiError, parse as parseV } from "valibot"; export const pipe = p @@ -12,8 +12,8 @@ export const E = { isLeft, isRight, tryCatchK, + getOrElse, + map } -export const O = {} - export const parse = tryCatchK(parseV, (e: unknown) => e as ValiError) diff --git a/tsconfig.json b/tsconfig.json index 1db31acf..ca1dde7b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,9 +30,22 @@ "@std": [ "src/std" ] - } + }, + "plugins": [ + { + "name": "@unsplash/ts-namespace-import-plugin", + "namespaces": { + "O": { + "importPath": "fp-ts/Option" + }, + "NEA": { + "importPath": "fp-ts/NonEmptyArray" + } + } + } + ] }, "include": [ "src/**/*" - ] + ], } \ No newline at end of file