tsondb 0.20.4 → 0.20.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -120,7 +120,7 @@ else {
120
120
  debug(`running command: serve`);
121
121
  validateConfigForServer(config);
122
122
  const db = await createDB(config, config.validationOptions, false);
123
- createServer(db, config.homeLayoutSections, config.serverOptions, config.validationOptions, config.customStylesheetPath, config.customAssetsPath);
123
+ createServer(db, config);
124
124
  break;
125
125
  }
126
126
  case "validate": {
@@ -1,3 +1,4 @@
1
+ import type { FormatterOptions as MarkdownFormatterOptions } from "@elyukai/markdown/format";
1
2
  import type { DefaultTSONDBTypes, ValidationOptions } from "./index.ts";
2
3
  import type { Output } from "./output.ts";
3
4
  import type { EntityDecl } from "./schema/dsl/index.ts";
@@ -18,6 +19,13 @@ export type Config<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> = {
18
19
  customStylesheetPath?: string;
19
20
  customAssetsPath?: string;
20
21
  validationOptions?: Partial<ValidationOptions>;
22
+ formattingOptions?: Partial<FormatterOptions>;
23
+ };
24
+ export type FormatterOptions = {
25
+ /**
26
+ * If set to false, Markdown formatting will be disabled. Otherwise, the default or, if provided, provided Markdown formatting options will be used to format Markdown strings.
27
+ */
28
+ markdown?: Partial<MarkdownFormatterOptions> | false;
21
29
  };
22
30
  export type ServerOptions = {
23
31
  port: number;
@@ -41,6 +49,7 @@ export declare const validateConfigForGeneration: (config: Config) => asserts co
41
49
  export type DataConfig<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> = Config<T> & {
42
50
  schema: Schema<T>;
43
51
  dataRootPath: string;
52
+ formattingOptions?: Partial<FormatterOptions>;
44
53
  };
45
54
  export declare const validateConfigForData: (config: Config) => asserts config is DataConfig;
46
55
  /**
@@ -53,6 +62,7 @@ export type ServerConfig<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> = Da
53
62
  validationOptions?: Partial<ValidationOptions>;
54
63
  customStylesheetPath?: string;
55
64
  customAssetsPath?: string;
65
+ formattingOptions?: Partial<FormatterOptions>;
56
66
  };
57
67
  /**
58
68
  * The configuration type required for validating the contents of the database.
@@ -1,4 +1,5 @@
1
1
  import type { InstanceContainer, InstanceContainerOverview } from "../shared/utils/instances.ts";
2
+ import type { FormatterOptions } from "./config.ts";
2
3
  import { Git } from "./git.js";
3
4
  import type { Output } from "./output.ts";
4
5
  import { type EntityDecl } from "./schema/dsl/index.ts";
@@ -42,6 +43,10 @@ export interface TSONDBOptions<T extends DefaultTSONDBTypes = DefaultTSONDBTypes
42
43
  * Options for validating the data in the database.
43
44
  */
44
45
  validationOptions?: Partial<ValidationOptions>;
46
+ /**
47
+ * Options for formatting the data in the database.
48
+ */
49
+ formatterOptions?: Partial<FormatterOptions>;
45
50
  }
46
51
  /**
47
52
  * Options for validating the data in the database.
@@ -96,6 +96,7 @@ export class TSONDB {
96
96
  #git;
97
97
  #referencesToInstances;
98
98
  #validationOptions;
99
+ #formatterOptions;
99
100
  #gitWrapper;
100
101
  #locked = false;
101
102
  constructor(options) {
@@ -106,6 +107,7 @@ export class TSONDB {
106
107
  this.#git = options.git;
107
108
  this.#referencesToInstances = options.referencesToInstances;
108
109
  this.#validationOptions = options.validationOptions;
110
+ this.#formatterOptions = options.formatterOptions;
109
111
  this.#gitWrapper = Lazy.of(() => this.#git
110
112
  ? new Git(this, this.#git.client, this.#git.root, this.#dataRootPath, () => this.#data, (data) => {
111
113
  this.#data = data;
@@ -281,7 +283,7 @@ export class TSONDB {
281
283
  await this.#data.forEachInstance(async (entityName, instance) => {
282
284
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
283
285
  const entity = this.#schema.getEntity(entityName);
284
- await writeInstance(this.#dataRootPath, entity, instance.id, instance.content);
286
+ await writeInstance(this.#dataRootPath, entity, instance.id, instance.content, this.#formatterOptions);
285
287
  }, true);
286
288
  debug("All data is formatted");
287
289
  }
@@ -331,7 +333,7 @@ export class TSONDB {
331
333
  throw new AggregateError(errors, "Validation errors occurred");
332
334
  }
333
335
  debug("Applying changes to disk ...");
334
- const diskResult = await applyStepsToDisk(this.#dataRootPath, steps);
336
+ const diskResult = await applyStepsToDisk(this.#dataRootPath, steps, this.#formatterOptions);
335
337
  if (isError(diskResult)) {
336
338
  debug("Error applying changes to disk: %s", diskResult.error.message);
337
339
  throw diskResult.error;
@@ -55,16 +55,16 @@ export type RegisteredEnumOrTypeAlias<Name extends string, T = Register> = Regis
55
55
  */
56
56
  export declare const Case: {
57
57
  <T extends string>(type: T): Case<T>;
58
- <T extends string, V extends NonNullable<unknown> | null>(type: T, value: V): Case<T, V>;
58
+ <T extends string, V extends AnyNonNullish | null>(type: T, value: V): Case<T, V>;
59
59
  };
60
60
  /**
61
61
  * Type representing an enum case object.
62
62
  */
63
63
  export type Case<K extends string, T = undefined> = {
64
- [P in K]: T extends NonNullable<unknown> | null ? {
64
+ [P in K]: T extends AnyNonNullish | null ? {
65
65
  [Key in ENUM_DISCRIMINATOR_KEY]: P;
66
66
  } & {
67
- [Key in P]: Extract<T, NonNullable<unknown> | null>;
67
+ [Key in P]: Extract<T, AnyNonNullish | null>;
68
68
  } : {
69
69
  [Key in ENUM_DISCRIMINATOR_KEY]: P;
70
70
  };
@@ -1,5 +1,6 @@
1
+ import type { FormatterOptions } from "../../config.ts";
1
2
  import type { Type } from "../dsl/index.ts";
2
3
  /**
3
4
  * Format the structure of a value to always look the same when serialized as JSON.
4
5
  */
5
- export declare const formatValue: (type: Type, value: unknown) => unknown;
6
+ export declare const formatValue: (type: Type, value: unknown, options: Partial<FormatterOptions> | undefined) => unknown;
@@ -1,3 +1,4 @@
1
+ import { format as formatMarkdown, formatInline as formatMarkdownInline, } from "@elyukai/markdown/format";
1
2
  import { mapObject, sortObjectKeys, sortObjectKeysByIndex } from "@elyukai/utils/object";
2
3
  import { assertExhaustive } from "@elyukai/utils/typeSafety";
3
4
  import { ENUM_DISCRIMINATOR_KEY } from "../../../shared/schema/declarations/EnumDecl.js";
@@ -11,19 +12,21 @@ const formatTranslationObjectValue = (type, value) => typeof value === "object"
11
12
  /**
12
13
  * Format the structure of a value to always look the same when serialized as JSON.
13
14
  */
14
- export const formatValue = (type, value) => {
15
+ export const formatValue = (type, value, options) => {
15
16
  switch (type.kind) {
16
17
  case NodeKind.ArrayType:
17
- return Array.isArray(value) ? value.map(item => formatValue(type.items, item)) : value;
18
+ return Array.isArray(value)
19
+ ? value.map(item => formatValue(type.items, item, options))
20
+ : value;
18
21
  case NodeKind.ObjectType:
19
22
  return typeof value === "object" && value !== null && !Array.isArray(value)
20
- ? sortObjectKeysByIndex(mapObject(value, (item, key) => type.properties[key] ? formatValue(type.properties[key].type, item) : item), Object.keys(type.properties))
23
+ ? sortObjectKeysByIndex(mapObject(value, (item, key) => type.properties[key] ? formatValue(type.properties[key].type, item, options) : item), Object.keys(type.properties))
21
24
  : value;
22
25
  case NodeKind.IncludeIdentifierType:
23
- return formatValue(type.reference.type.value, value);
26
+ return formatValue(type.reference.type.value, value, options);
24
27
  case NodeKind.NestedEntityMapType:
25
28
  return typeof value === "object" && value !== null && !Array.isArray(value)
26
- ? sortObjectKeys(mapObject(value, item => formatValue(type.type.value, item)))
29
+ ? sortObjectKeys(mapObject(value, item => formatValue(type.type.value, item, options)))
27
30
  : value;
28
31
  case NodeKind.EnumType: {
29
32
  if (typeof value === "object" &&
@@ -38,7 +41,7 @@ export const formatValue = (type, value) => {
38
41
  [ENUM_DISCRIMINATOR_KEY]: caseName,
39
42
  ...(caseValue == null || caseType == null
40
43
  ? {}
41
- : { [caseName]: formatValue(caseType, caseValue) }),
44
+ : { [caseName]: formatValue(caseType, caseValue, options) }),
42
45
  };
43
46
  }
44
47
  return value;
@@ -47,11 +50,25 @@ export const formatValue = (type, value) => {
47
50
  return Array.isArray(value) ? value.toSorted() : value;
48
51
  case NodeKind.TranslationObjectType:
49
52
  return formatTranslationObjectValue(type.properties, value);
53
+ case NodeKind.StringType: {
54
+ if (options?.markdown === false) {
55
+ return value;
56
+ }
57
+ switch (type.markdown) {
58
+ case "inline":
59
+ return typeof value === "string" ? formatMarkdownInline(value.trim()) : value;
60
+ case "block":
61
+ return typeof value === "string"
62
+ ? formatMarkdown(value.trimEnd(), options?.markdown)
63
+ : value;
64
+ default:
65
+ return value;
66
+ }
67
+ }
50
68
  case NodeKind.BooleanType:
51
69
  case NodeKind.DateType:
52
70
  case NodeKind.FloatType:
53
71
  case NodeKind.IntegerType:
54
- case NodeKind.StringType:
55
72
  case NodeKind.TypeArgumentType:
56
73
  case NodeKind.ReferenceIdentifierType:
57
74
  return value;
@@ -1,4 +1,4 @@
1
- import type { HomeLayoutSection } from "../config.ts";
1
+ import type { HomeLayoutSection, ServerConfig } from "../config.ts";
2
2
  import type { TSONDB, ValidationOptions } from "../index.ts";
3
3
  export type ServerOptions = {
4
4
  port: number;
@@ -16,4 +16,4 @@ declare global {
16
16
  }
17
17
  }
18
18
  }
19
- export declare const createServer: (db: TSONDB, homeLayoutSections?: HomeLayoutSection[], options?: Partial<ServerOptions>, validationOptions?: Partial<ValidationOptions>, customStylesheetPath?: string, customAssetsPath?: string) => void;
19
+ export declare const createServer: (db: TSONDB, config: Omit<ServerConfig, "locales">) => void;
@@ -15,7 +15,8 @@ const staticNodeModule = (moduleName) => {
15
15
  }
16
16
  return express.static(dirname(pathToPackageJson));
17
17
  };
18
- export const createServer = (db, homeLayoutSections, options, validationOptions, customStylesheetPath, customAssetsPath) => {
18
+ export const createServer = (db, config) => {
19
+ const { serverOptions: options, homeLayoutSections, validationOptions, customAssetsPath, customStylesheetPath, } = config;
19
20
  const { port } = { ...defaultOptions, ...options };
20
21
  const app = express();
21
22
  app.use(express.static(join(import.meta.dirname, "../../../../public")));
@@ -1,2 +1,3 @@
1
+ import type { FormatterOptions } from "../config.ts";
1
2
  import type { TransactionStep } from "../transaction.ts";
2
- export declare const applyStepsToDisk: (root: string, steps: TransactionStep[]) => Promise<import("@elyukai/utils/result").Ok<void> | import("@elyukai/utils/result").Error<Error>>;
3
+ export declare const applyStepsToDisk: (root: string, steps: TransactionStep[], formatterOptions: Partial<FormatterOptions> | undefined) => Promise<import("@elyukai/utils/result").Error<Error> | import("@elyukai/utils/result").Ok<void>>;
@@ -1,8 +1,8 @@
1
1
  import { error, isError, ok } from "@elyukai/utils/result";
2
2
  import * as DatabaseFilesystem from "./files.js";
3
- const setInstanceOnDisk = async (root, entity, instanceId, instanceContent) => {
3
+ const setInstanceOnDisk = async (root, entity, instanceId, instanceContent, formatterOptions) => {
4
4
  try {
5
- await DatabaseFilesystem.writeInstance(root, entity, instanceId, instanceContent);
5
+ await DatabaseFilesystem.writeInstance(root, entity, instanceId, instanceContent, formatterOptions);
6
6
  return ok();
7
7
  }
8
8
  catch (e) {
@@ -18,41 +18,41 @@ const deleteInstanceOnDisk = async (root, entityName, instanceId) => {
18
18
  return error(e);
19
19
  }
20
20
  };
21
- const rollbackChanges = async (root, steps) => {
21
+ const rollbackChanges = async (root, steps, formatterOptions) => {
22
22
  for (const step of steps) {
23
- const res = await runReverseStepAction(root, step);
23
+ const res = await runReverseStepAction(root, step, formatterOptions);
24
24
  if (isError(res)) {
25
25
  return res;
26
26
  }
27
27
  }
28
28
  return ok();
29
29
  };
30
- const runStepAction = (root, step) => {
30
+ const runStepAction = (root, step, formatterOptions) => {
31
31
  switch (step.kind) {
32
32
  case "create":
33
33
  case "update":
34
- return setInstanceOnDisk(root, step.entity, step.instanceId, step.instanceContent);
34
+ return setInstanceOnDisk(root, step.entity, step.instanceId, step.instanceContent, formatterOptions);
35
35
  case "delete":
36
36
  return deleteInstanceOnDisk(root, step.entity.name, step.instanceId);
37
37
  }
38
38
  };
39
- const runReverseStepAction = (root, step) => {
39
+ const runReverseStepAction = (root, step, formatterOptions) => {
40
40
  switch (step.kind) {
41
41
  case "create":
42
42
  return deleteInstanceOnDisk(root, step.entity.name, step.instanceId);
43
43
  case "update":
44
- return setInstanceOnDisk(root, step.entity, step.instanceId, step.oldInstance);
44
+ return setInstanceOnDisk(root, step.entity, step.instanceId, step.oldInstance, formatterOptions);
45
45
  case "delete":
46
- return setInstanceOnDisk(root, step.entity, step.instanceId, step.oldInstance);
46
+ return setInstanceOnDisk(root, step.entity, step.instanceId, step.oldInstance, formatterOptions);
47
47
  }
48
48
  };
49
- export const applyStepsToDisk = async (root, steps) => {
49
+ export const applyStepsToDisk = async (root, steps, formatterOptions) => {
50
50
  for (let i = 0; i < steps.length; i++) {
51
51
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
52
52
  const step = steps[i];
53
- const res = await runStepAction(root, step);
53
+ const res = await runStepAction(root, step, formatterOptions);
54
54
  if (isError(res)) {
55
- await rollbackChanges(root, steps.slice(0, i));
55
+ await rollbackChanges(root, steps.slice(0, i), formatterOptions);
56
56
  return res;
57
57
  }
58
58
  }
@@ -1,7 +1,8 @@
1
1
  import type { InstanceContent } from "../../shared/utils/instances.ts";
2
+ import type { FormatterOptions } from "../config.ts";
2
3
  import { type EntityDecl } from "../schema/dsl/index.ts";
3
4
  export declare const getFileNameForId: (id: string) => string;
4
5
  export declare const getPathToInstance: (dataRoot: string, entityName: string, id: string) => string;
5
- export declare const writeInstance: (dataRoot: string, entity: EntityDecl, id: string, instance: InstanceContent) => Promise<void>;
6
+ export declare const writeInstance: (dataRoot: string, entity: EntityDecl, id: string, instance: InstanceContent, formatterOptions: Partial<FormatterOptions> | undefined) => Promise<void>;
6
7
  export declare const deleteInstance: (dataRoot: string, entityName: string, id: string) => Promise<void>;
7
- export declare const formatInstance: (entity: EntityDecl, instanceContent: InstanceContent) => string;
8
+ export declare const formatInstance: (entity: EntityDecl, instanceContent: InstanceContent, options: Partial<FormatterOptions> | undefined) => string;
@@ -4,8 +4,8 @@ import {} from "../schema/dsl/index.js";
4
4
  import { formatValue } from "../schema/treeOperations/format.js";
5
5
  export const getFileNameForId = (id) => `${id}.json`;
6
6
  export const getPathToInstance = (dataRoot, entityName, id) => join(dataRoot, entityName, getFileNameForId(id));
7
- export const writeInstance = (dataRoot, entity, id, instance) => writeFile(getPathToInstance(dataRoot, entity.name, id), formatInstance(entity, instance), {
7
+ export const writeInstance = (dataRoot, entity, id, instance, formatterOptions) => writeFile(getPathToInstance(dataRoot, entity.name, id), formatInstance(entity, instance, formatterOptions), {
8
8
  encoding: "utf-8",
9
9
  });
10
10
  export const deleteInstance = (dataRoot, entityName, id) => rm(getPathToInstance(dataRoot, entityName, id));
11
- export const formatInstance = (entity, instanceContent) => JSON.stringify(formatValue(entity.type.value, instanceContent), undefined, 2) + "\n";
11
+ export const formatInstance = (entity, instanceContent, options) => JSON.stringify(formatValue(entity.type.value, instanceContent, options), undefined, 2) + "\n";
@@ -2,7 +2,7 @@ import { anySameIndices } from "@elyukai/utils/array/filters";
2
2
  import { flatCombine } from "@elyukai/utils/array/transformations";
3
3
  import { deepEqual } from "@elyukai/utils/equality";
4
4
  import { error, isError, mapError, ok } from "@elyukai/utils/result";
5
- import { getValueAtKeyPath, renderKeyPath } from "../../shared/schema/utils/keyPath.js";
5
+ import { getValueAtKeyPath, getValueAtKeyPathIfDefined, renderKeyPath, } from "../../shared/schema/utils/keyPath.js";
6
6
  import {} from "./databaseInMemory.js";
7
7
  export class UniqueConstraintError extends Error {
8
8
  parts;
@@ -42,7 +42,7 @@ export const checkUniqueConstraintsForEntity = (entity, instances, instanceOverv
42
42
  const index = instances.flatMap(({ id, content }) => flatCombine(normalizedConstraint.map(elem => {
43
43
  if ("keyPath" in elem) {
44
44
  return [
45
- getValueAtKeyPath(content, elem.keyPath) ??
45
+ getValueAtKeyPathIfDefined(content, elem.keyPath) ??
46
46
  (elem.keyPathFallback
47
47
  ? getValueAtKeyPath(content, elem.keyPathFallback)
48
48
  : undefined),
@@ -51,7 +51,7 @@ export const checkUniqueConstraintsForEntity = (entity, instances, instanceOverv
51
51
  else {
52
52
  return Object.entries(getValueAtKeyPath(content, elem.entityMapKeyPath)).map(([nestedId, nestedContent]) => [
53
53
  nestedId,
54
- getValueAtKeyPath(nestedContent, elem.keyPathInEntityMap) ??
54
+ getValueAtKeyPathIfDefined(nestedContent, elem.keyPathInEntityMap) ??
55
55
  (elem.keyPathInEntityMapFallback
56
56
  ? getValueAtKeyPath(nestedContent, elem.keyPathInEntityMapFallback)
57
57
  : undefined),
@@ -14,3 +14,4 @@ export declare const renderKeyPath: (keyPath: KeyPath) => string;
14
14
  export declare const renderParsedKeyPath: (keyPath: ParsedKeyPath) => string;
15
15
  export declare const getAtKeyPath: <T>(value: T, previousPath: ParsedKeyPath, remainingKeyPath: ParsedKeyPath, throwOnPathMismatch: boolean, fArray: (value: T, index: number) => Result<T, (previousPath: string) => string>, fObject: (value: T, key: string) => Result<T, (previousPath: string) => string>, ...fs: ((value: T, key: KeyPathElement) => Result<[T, skipKey?: boolean], void>)[]) => T;
16
16
  export declare const getValueAtKeyPath: (value: unknown, keyPath: KeyPath, throwOnPathMismatch?: boolean) => unknown;
17
+ export declare const getValueAtKeyPathIfDefined: (value: unknown, keyPath: KeyPath) => unknown;
@@ -1,6 +1,6 @@
1
1
  import { isNotEmpty } from "@elyukai/utils/array/nonEmpty";
2
2
  import { error, isOk, ok, reduce } from "@elyukai/utils/result";
3
- import { assertExhaustive } from "@elyukai/utils/typeSafety";
3
+ import { assertExhaustive, trySafe } from "@elyukai/utils/typeSafety";
4
4
  const intPattern = /^(?:0|[1-9][0-9])*$/;
5
5
  export const parseKeyPath = (keyPath) => normalizeKeyPath(keyPath).map(part => {
6
6
  if (intPattern.test(part)) {
@@ -63,3 +63,4 @@ export const getValueAtKeyPath = (value, keyPath, throwOnPathMismatch = false) =
63
63
  ? ok(value[name])
64
64
  : error(previousPath => `Object at key path "${previousPath}" does not contain the key ${name}.`)
65
65
  : error(previousPath => `Key path "${previousPath}" does not contain an object.`));
66
+ export const getValueAtKeyPathIfDefined = (value, keyPath) => trySafe(() => getValueAtKeyPath(value, keyPath, true));
@@ -1,6 +1,8 @@
1
+ import type { MarkdownStringOption } from "../schema/types/StringType.ts";
1
2
  export interface StringConstraints {
2
3
  minLength?: number;
3
4
  maxLength?: number;
4
5
  pattern?: string | RegExp;
6
+ markdown?: MarkdownStringOption;
5
7
  }
6
8
  export declare const validateStringConstraints: (constraints: StringConstraints, value: string) => Error[];
@@ -1,3 +1,5 @@
1
+ import { validate, validateInline } from "@elyukai/markdown/validate";
2
+ import { assertExhaustive } from "@elyukai/utils/typeSafety";
1
3
  import { parallelizeErrors } from "../utils/validation.js";
2
4
  export const validateStringConstraints = (constraints, value) => parallelizeErrors([
3
5
  constraints.minLength !== undefined && value.length < constraints.minLength
@@ -17,4 +19,17 @@ export const validateStringConstraints = (constraints, value) => parallelizeErro
17
19
  ? TypeError(`string does not match the pattern ${pattern.toString()}`)
18
20
  : undefined;
19
21
  })(),
22
+ (() => {
23
+ if (constraints.markdown === undefined) {
24
+ return undefined;
25
+ }
26
+ switch (constraints.markdown) {
27
+ case "inline":
28
+ return validateInline(value);
29
+ case "block":
30
+ return validate(value);
31
+ default:
32
+ return assertExhaustive(constraints.markdown);
33
+ }
34
+ })(),
20
35
  ]);