tsondb 0.4.0 → 0.5.1

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.
Files changed (44) hide show
  1. package/lib/bin/tsondb.d.ts +3 -1
  2. package/lib/bin/tsondb.js +9 -8
  3. package/lib/node/Schema.js +5 -4
  4. package/lib/node/index.d.ts +1 -0
  5. package/lib/node/index.js +20 -7
  6. package/lib/node/renderers/jsonschema/index.js +7 -0
  7. package/lib/node/renderers/jsonschema/render.js +6 -1
  8. package/lib/node/renderers/ts/render.js +4 -1
  9. package/lib/node/schema/Node.d.ts +1 -1
  10. package/lib/node/schema/Node.js +4 -3
  11. package/lib/node/schema/declarations/EntityDecl.d.ts +3 -3
  12. package/lib/node/schema/declarations/EntityDecl.js +2 -3
  13. package/lib/node/schema/declarations/EnumDecl.d.ts +1 -1
  14. package/lib/node/schema/declarations/EnumDecl.js +3 -10
  15. package/lib/node/schema/declarations/TypeAliasDecl.js +3 -11
  16. package/lib/node/schema/types/Type.d.ts +7 -1
  17. package/lib/node/schema/types/Type.js +14 -4
  18. package/lib/node/schema/types/generic/ArrayType.js +2 -2
  19. package/lib/node/schema/types/generic/EnumType.js +6 -2
  20. package/lib/node/schema/types/generic/ObjectType.js +6 -3
  21. package/lib/node/schema/types/references/NestedEntityMapType.js +5 -13
  22. package/lib/node/server/api/declarations.js +6 -2
  23. package/lib/node/server/api/git.js +1 -2
  24. package/lib/node/server/api/instanceOperations.js +5 -3
  25. package/lib/node/server/index.d.ts +1 -1
  26. package/lib/node/server/index.js +28 -9
  27. package/lib/node/server/init.js +1 -0
  28. package/lib/node/utils/instances.d.ts +1 -0
  29. package/lib/node/utils/instances.js +2 -0
  30. package/lib/shared/api.d.ts +1 -1
  31. package/lib/shared/utils/array.d.ts +1 -0
  32. package/lib/shared/utils/array.js +1 -0
  33. package/lib/shared/utils/displayName.js +3 -0
  34. package/lib/shared/utils/object.js +4 -1
  35. package/lib/web/api.d.ts +1 -1
  36. package/lib/web/api.js +1 -4
  37. package/lib/web/components/Git.js +1 -1
  38. package/lib/web/hooks/useAPIResource.js +2 -1
  39. package/lib/web/hooks/useInstanceNamesByEntity.js +2 -1
  40. package/lib/web/hooks/useMappedAPIResource.js +3 -1
  41. package/lib/web/routes/Entity.js +3 -3
  42. package/lib/web/routes/Home.js +2 -1
  43. package/package.json +3 -2
  44. package/public/css/styles.css +64 -13
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import type { Output } from "../node/renderers/Output.ts";
3
3
  import type { Schema } from "../node/Schema.ts";
4
+ import type { ServerOptions } from "../node/server/index.ts";
4
5
  export type Config = {
6
+ serverOptions?: ServerOptions;
5
7
  schema: Schema;
6
8
  outputs: Output[];
7
- dataRootPath?: string;
9
+ dataRootPath: string;
8
10
  };
package/lib/bin/tsondb.js CHANGED
@@ -12,7 +12,7 @@ import { access, constants } from "node:fs/promises";
12
12
  import { join } from "node:path";
13
13
  import { cwd } from "node:process";
14
14
  import { parseArguments } from "simple-cli-args";
15
- import { generateOutputs, serve, validate } from "../node/index.js";
15
+ import { format, generateOutputs, serve, validate } from "../node/index.js";
16
16
  const debug = Debug("tsondb:cli");
17
17
  const passedArguments = parseArguments({
18
18
  commands: {
@@ -25,6 +25,9 @@ const passedArguments = parseArguments({
25
25
  serve: {
26
26
  name: "serve",
27
27
  },
28
+ format: {
29
+ name: "format",
30
+ },
28
31
  },
29
32
  });
30
33
  // import the config
@@ -68,16 +71,14 @@ switch (passedArguments.command.name) {
68
71
  break;
69
72
  case "serve":
70
73
  debug(`running command: serve`);
71
- if (config.dataRootPath === undefined) {
72
- throw new Error("No dataRootPath specified in config");
73
- }
74
- await serve(config.schema, config.dataRootPath);
74
+ await serve(config.schema, config.dataRootPath, config.serverOptions);
75
75
  break;
76
76
  case "validate":
77
77
  debug(`running command: validate`);
78
- if (config.dataRootPath === undefined) {
79
- throw new Error("No dataRootPath specified in config");
80
- }
81
78
  await validate(config.schema, config.dataRootPath);
82
79
  break;
80
+ case "format":
81
+ debug(`running command: format`);
82
+ await format(config.schema, config.dataRootPath);
83
+ break;
83
84
  }
@@ -4,10 +4,11 @@ import { isStringType } from "./schema/types/primitives/StringType.js";
4
4
  import { isNestedEntityMapType } from "./schema/types/references/NestedEntityMapType.js";
5
5
  import { findTypeAtPath } from "./schema/types/Type.js";
6
6
  const checkDuplicateIdentifier = (existingDecls, decl) => {
7
- if (existingDecls
7
+ const existingDeclWithSameName = existingDecls
8
8
  .values()
9
- .some(otherDecl => otherDecl !== decl && otherDecl.name.toLowerCase() === decl.name.toLowerCase())) {
10
- throw new Error(`Duplicate declaration name: "${decl.name}". Make sure declaration names are globally unique.`);
9
+ .find(otherDecl => otherDecl !== decl && otherDecl.name.toLowerCase() === decl.name.toLowerCase());
10
+ if (existingDeclWithSameName) {
11
+ throw new Error(`Duplicate declaration name "${decl.name}" in "${decl.sourceUrl}" and "${existingDeclWithSameName.sourceUrl}". Make sure declaration names are globally unique.`);
11
12
  }
12
13
  };
13
14
  const checkParameterNamesShadowing = (decls) => {
@@ -21,7 +22,7 @@ const checkParameterNamesShadowing = (decls) => {
21
22
  };
22
23
  const checkEntityDisplayNamePaths = (decls, localeEntity) => {
23
24
  for (const decl of decls) {
24
- if (isEntityDecl(decl)) {
25
+ if (isEntityDecl(decl) && decl.displayName !== null) {
25
26
  const displayName = decl.displayName ?? "name";
26
27
  if (typeof displayName === "object") {
27
28
  const pathToLocaleMap = displayName.pathToLocaleMap ?? "translations";
@@ -6,3 +6,4 @@ export declare const validate: (schema: Schema, dataRootPath: string) => Promise
6
6
  export declare const generateAndValidate: (schema: Schema, outputs: Output[], dataRootPath: string) => Promise<void>;
7
7
  export declare const serve: (schema: Schema, dataRootPath: string, serverOptions?: Partial<ServerOptions>) => Promise<void>;
8
8
  export declare const generateValidateAndServe: (schema: Schema, outputs: Output[], dataRootPath: string, serverOptions?: Partial<ServerOptions>) => Promise<void>;
9
+ export declare const format: (schema: Schema, dataRootPath: string) => Promise<void>;
package/lib/node/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import Debug from "debug";
2
- import { mkdir } from "fs/promises";
2
+ import { mkdir, writeFile } from "fs/promises";
3
3
  import { join } from "path";
4
4
  import { parallelizeErrors } from "../shared/utils/validation.js";
5
5
  import { getEntities } from "./Schema.js";
6
6
  import { createValidators, validateEntityDecl } from "./schema/index.js";
7
7
  import { createServer } from "./server/index.js";
8
8
  import { getErrorMessageForDisplay, wrapErrorsIfAny } from "./utils/error.js";
9
- import { getInstancesByEntityName } from "./utils/instances.js";
9
+ import { formatInstance, getInstancesByEntityName } from "./utils/instances.js";
10
10
  const debug = Debug("tsondb:schema");
11
11
  const prepareFolders = async (dataRootPath, entities) => {
12
12
  await mkdir(dataRootPath, { recursive: true });
@@ -20,8 +20,8 @@ export const generateOutputs = async (schema, outputs) => {
20
20
  await output.run(schema);
21
21
  }
22
22
  };
23
- const _validate = (entities, instancesByEntityName) => {
24
- const errors = entities.flatMap(entity => parallelizeErrors(instancesByEntityName[entity.name]?.map(instance => wrapErrorsIfAny(`in file "${entity.name}/${instance.fileName}"`, validateEntityDecl(createValidators(instancesByEntityName), entity, instance.content))) ?? []));
23
+ const _validate = (dataRootPath, entities, instancesByEntityName) => {
24
+ const errors = entities.flatMap(entity => parallelizeErrors(instancesByEntityName[entity.name]?.map(instance => wrapErrorsIfAny(`in file "${join(dataRootPath, entity.name, instance.fileName)}"`, validateEntityDecl(createValidators(instancesByEntityName), entity, instance.content))) ?? []));
25
25
  if (errors.length === 0) {
26
26
  debug("All entities are valid");
27
27
  }
@@ -37,14 +37,14 @@ export const validate = async (schema, dataRootPath) => {
37
37
  const entities = getEntities(schema);
38
38
  await prepareFolders(dataRootPath, entities);
39
39
  const instancesByEntityName = await getInstancesByEntityName(dataRootPath, entities);
40
- _validate(entities, instancesByEntityName);
40
+ _validate(dataRootPath, entities, instancesByEntityName);
41
41
  };
42
42
  export const generateAndValidate = async (schema, outputs, dataRootPath) => {
43
43
  await generateOutputs(schema, outputs);
44
44
  const entities = getEntities(schema);
45
45
  await prepareFolders(dataRootPath, entities);
46
46
  const instancesByEntityName = await getInstancesByEntityName(dataRootPath, entities);
47
- _validate(entities, instancesByEntityName);
47
+ _validate(dataRootPath, entities, instancesByEntityName);
48
48
  };
49
49
  export const serve = async (schema, dataRootPath, serverOptions) => {
50
50
  const entities = getEntities(schema);
@@ -57,6 +57,19 @@ export const generateValidateAndServe = async (schema, outputs, dataRootPath, se
57
57
  const entities = getEntities(schema);
58
58
  await prepareFolders(dataRootPath, entities);
59
59
  const instancesByEntityName = await getInstancesByEntityName(dataRootPath, entities);
60
- _validate(entities, instancesByEntityName);
60
+ _validate(dataRootPath, entities, instancesByEntityName);
61
61
  await createServer(schema, dataRootPath, instancesByEntityName, serverOptions);
62
62
  };
63
+ export const format = async (schema, dataRootPath) => {
64
+ const entities = getEntities(schema);
65
+ await prepareFolders(dataRootPath, entities);
66
+ const instancesByEntityName = await getInstancesByEntityName(dataRootPath, entities);
67
+ for (const entityName in instancesByEntityName) {
68
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
69
+ const entity = entities.find(entity => entity.name === entityName);
70
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
71
+ for (const instance of instancesByEntityName[entityName]) {
72
+ await writeFile(join(dataRootPath, entityName, instance.fileName), formatInstance(entity, instance.content), { encoding: "utf-8" });
73
+ }
74
+ }
75
+ };
@@ -1,17 +1,21 @@
1
+ import Debug from "debug";
1
2
  import { mkdir, rm, writeFile } from "node:fs/promises";
2
3
  import { basename, dirname, extname, join, relative } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { commonPrefix } from "../../../shared/utils/string.js";
5
6
  import { groupDeclarationsBySourceUrl, resolveTypeArgumentsInDecls } from "../../schema/index.js";
6
7
  import { render } from "./render.js";
8
+ const debug = Debug("tsondb:renderer:jsonschema");
7
9
  const extension = ".schema.json";
8
10
  export const JsonSchemaOutput = (options) => ({
9
11
  run: async (schema) => {
10
12
  if (options.rendererOptions?.preserveFiles === true) {
13
+ debug("emitting declarations to multiple files...");
11
14
  await rm(options.targetPath, { recursive: true, force: true });
12
15
  await mkdir(options.targetPath, { recursive: true });
13
16
  const declarationsBySourceUrl = groupDeclarationsBySourceUrl(resolveTypeArgumentsInDecls(schema.declarations));
14
17
  const sourceRootPath = fileURLToPath(commonPrefix(...Object.keys(declarationsBySourceUrl)));
18
+ debug("common source root path: %s", sourceRootPath);
15
19
  if (sourceRootPath) {
16
20
  for (const [sourceUrl, decls] of Object.entries(declarationsBySourceUrl)) {
17
21
  const sourcePath = fileURLToPath(sourceUrl);
@@ -23,13 +27,16 @@ export const JsonSchemaOutput = (options) => ({
23
27
  encoding: "utf-8",
24
28
  });
25
29
  }
30
+ debug("emitted declaration files to %s", options.targetPath);
26
31
  }
27
32
  }
28
33
  else {
34
+ debug("emitting declarations to single file...");
29
35
  await mkdir(dirname(options.targetPath), { recursive: true });
30
36
  await writeFile(options.targetPath, render(options.rendererOptions, resolveTypeArgumentsInDecls(schema.declarations)), {
31
37
  encoding: "utf-8",
32
38
  });
39
+ debug("emitted declarations to %s", options.targetPath);
33
40
  }
34
41
  },
35
42
  });
@@ -162,8 +162,13 @@ const renderDeclarations = (options, declarations) => Object.fromEntries(declara
162
162
  export const render = (options = defaultOptions, declarations) => {
163
163
  const finalOptions = { ...defaultOptions, ...options };
164
164
  return JSON.stringify({
165
- $defs: renderDeclarations(finalOptions, flatMapAuxiliaryDecls(node => {
165
+ $defs: renderDeclarations(finalOptions, flatMapAuxiliaryDecls((node, existingDecls) => {
166
166
  if (isNestedEntityMapType(node)) {
167
+ if (existingDecls.some(decl => decl.name === node.name)) {
168
+ // this may happen when a nested entity map is defined in a generic declaration and the generic declaration is used multiple times
169
+ // TODO: circumvent by defining the nested entity declaration outside the generic declaration
170
+ return undefined;
171
+ }
167
172
  return TypeAliasDecl(getParentDecl(node)?.sourceUrl ?? "", {
168
173
  name: node.name,
169
174
  comment: node.comment,
@@ -1,6 +1,7 @@
1
1
  import { EOL } from "node:os";
2
2
  import { dirname, relative } from "node:path";
3
3
  import { discriminatorKey } from "../../../shared/enum.js";
4
+ import { unique } from "../../../shared/utils/array.js";
4
5
  import { toCamelCase } from "../../../shared/utils/string.js";
5
6
  import { assertExhaustive } from "../../../shared/utils/typeSafety.js";
6
7
  import { addEphemeralUUIDToType, createEntityIdentifierTypeAsDecl, isEntityDecl, } from "../../schema/declarations/EntityDecl.js";
@@ -110,7 +111,9 @@ const renderImports = (currentUrl, imports) => {
110
111
  names,
111
112
  ])
112
113
  .toSorted(([sourceUrlA], [sourceUrlB]) => sourceUrlA.localeCompare(sourceUrlB))
113
- .map(([sourceUrl, names]) => `import { ${names.toSorted((a, b) => a.localeCompare(b)).join(", ")} } from "${sourceUrl}"`)
114
+ .map(([sourceUrl, names]) => `import { ${unique(names)
115
+ .toSorted((a, b) => a.localeCompare(b))
116
+ .join(", ")} } from "${sourceUrl}"`)
114
117
  .join(EOL);
115
118
  return importsSyntax.length > 0 ? importsSyntax + EOL + EOL : "";
116
119
  };
@@ -26,7 +26,7 @@ export interface BaseNode {
26
26
  kind: (typeof NodeKind)[keyof typeof NodeKind];
27
27
  }
28
28
  export type Node = Decl | Type;
29
- export declare const flatMapAuxiliaryDecls: (callbackFn: (node: Node) => (Decl | undefined)[] | Decl | undefined, declarations: readonly Decl[]) => Decl[];
29
+ export declare const flatMapAuxiliaryDecls: (callbackFn: (node: Node, existingDecls: Decl[]) => (Decl | undefined)[] | Decl | undefined, declarations: readonly Decl[]) => Decl[];
30
30
  export type IdentifierToCheck = {
31
31
  name: string;
32
32
  value: unknown;
@@ -62,11 +62,12 @@ export const flatMapAuxiliaryDecls = (callbackFn, declarations) => {
62
62
  }
63
63
  };
64
64
  const reducer = (node, decls) => {
65
- const result = callbackFn(node);
65
+ const result = callbackFn(node, decls);
66
66
  const normalizedResult = (Array.isArray(result) ? result : [result]).filter(decl => decl !== undefined);
67
67
  normalizedResult.forEach(decl => {
68
- if (decls.some(existingDecl => existingDecl.name === decl.name)) {
69
- throw new Error(`Duplicate declaration name: "${decl.name}". Make sure declaration names are globally unique.`);
68
+ const existingDeclWithSameName = decls.find(existingDecl => existingDecl !== decl && existingDecl.name === decl.name);
69
+ if (existingDeclWithSameName) {
70
+ throw new Error(`Duplicate declaration name: "${decl.name}" in "${decl.sourceUrl}" and "${existingDeclWithSameName.sourceUrl}". Make sure declaration names are globally unique.`);
70
71
  }
71
72
  });
72
73
  return decls.concat(normalizedResult);
@@ -24,7 +24,7 @@ export interface EntityDecl<Name extends string = string, T extends ObjectType =
24
24
  * @default "name"
25
25
  */
26
26
  pathInLocaleMap?: string;
27
- };
27
+ } | null;
28
28
  isDeprecated?: boolean;
29
29
  }
30
30
  export interface SerializedEntityDecl<Name extends string = string, T extends SerializedObjectType = SerializedObjectType> extends SerializedBaseDecl<Name, []> {
@@ -43,7 +43,7 @@ export interface SerializedEntityDecl<Name extends string = string, T extends Se
43
43
  * @default "name"
44
44
  */
45
45
  pathInLocaleMap?: string;
46
- };
46
+ } | null;
47
47
  isDeprecated?: boolean;
48
48
  }
49
49
  export declare const EntityDecl: <Name extends string, T extends ObjectType>(sourceUrl: string, options: {
@@ -63,7 +63,7 @@ export declare const EntityDecl: <Name extends string, T extends ObjectType>(sou
63
63
  * @default "name"
64
64
  */
65
65
  pathInLocaleMap?: string;
66
- };
66
+ } | null;
67
67
  isDeprecated?: boolean;
68
68
  }) => EntityDecl<Name, T>;
69
69
  export { EntityDecl as Entity };
@@ -2,7 +2,7 @@ import { Lazy } from "../../../shared/utils/lazy.js";
2
2
  import { NodeKind } from "../Node.js";
3
3
  import { getNestedDeclarationsInObjectType, getReferencesForObjectType, Required, resolveTypeArgumentsInObjectType, serializeObjectType, } from "../types/generic/ObjectType.js";
4
4
  import { StringType } from "../types/primitives/StringType.js";
5
- import { validate } from "../types/Type.js";
5
+ import { setParent, validate } from "../types/Type.js";
6
6
  import { validateDeclName } from "./Declaration.js";
7
7
  import { TypeAliasDecl } from "./TypeAliasDecl.js";
8
8
  export const EntityDecl = (sourceUrl, options) => {
@@ -19,8 +19,7 @@ export const EntityDecl = (sourceUrl, options) => {
19
19
  throw new TypeError(`Invalid object key "${key}" for entity "${options.name}". The key "id" is reserved for the entity identifier.`);
20
20
  }
21
21
  });
22
- type.parent = decl;
23
- return type;
22
+ return setParent(type, decl);
24
23
  }),
25
24
  };
26
25
  return decl;
@@ -4,7 +4,7 @@ import { NodeKind } from "../Node.js";
4
4
  import type { SerializedTypeParameter, TypeParameter } from "../TypeParameter.js";
5
5
  import type { EnumCaseDecl, SerializedEnumCaseDecl, SerializedEnumType } from "../types/generic/EnumType.js";
6
6
  import { EnumType } from "../types/generic/EnumType.js";
7
- import type { Type } from "../types/Type.js";
7
+ import { type Type } from "../types/Type.js";
8
8
  import type { ValidatorHelpers } from "../validation/type.js";
9
9
  import type { BaseDecl, GetNestedDeclarations, SerializedBaseDecl, TypeArguments } from "./Declaration.js";
10
10
  export interface EnumDecl<Name extends string = string, T extends Record<string, EnumCaseDecl> = Record<string, EnumCaseDecl>, Params extends TypeParameter[] = TypeParameter[]> extends BaseDecl<Name, Params> {
@@ -2,6 +2,7 @@ import { Lazy } from "../../../shared/utils/lazy.js";
2
2
  import { NodeKind } from "../Node.js";
3
3
  import { serializeTypeParameter } from "../TypeParameter.js";
4
4
  import { EnumType, getNestedDeclarationsInEnumType, getReferencesForEnumType, resolveTypeArgumentsInEnumType, serializeEnumType, validateEnumType, } from "../types/generic/EnumType.js";
5
+ import { setParent } from "../types/Type.js";
5
6
  import { getTypeArgumentsRecord, validateDeclName } from "./Declaration.js";
6
7
  export const GenEnumDecl = (sourceUrl, options) => {
7
8
  validateDeclName(options.name);
@@ -9,11 +10,7 @@ export const GenEnumDecl = (sourceUrl, options) => {
9
10
  ...options,
10
11
  kind: NodeKind.EnumDecl,
11
12
  sourceUrl,
12
- type: Lazy.of(() => {
13
- const type = EnumType(options.values(...options.parameters));
14
- type.parent = decl;
15
- return type;
16
- }),
13
+ type: Lazy.of(() => setParent(EnumType(options.values(...options.parameters)), decl)),
17
14
  };
18
15
  return decl;
19
16
  };
@@ -25,11 +22,7 @@ export const EnumDecl = (sourceUrl, options) => {
25
22
  kind: NodeKind.EnumDecl,
26
23
  sourceUrl,
27
24
  parameters: [],
28
- type: Lazy.of(() => {
29
- const type = EnumType(options.values());
30
- type.parent = decl;
31
- return type;
32
- }),
25
+ type: Lazy.of(() => setParent(EnumType(options.values()), decl)),
33
26
  };
34
27
  return decl;
35
28
  };
@@ -1,7 +1,7 @@
1
1
  import { Lazy } from "../../../shared/utils/lazy.js";
2
2
  import { NodeKind } from "../Node.js";
3
3
  import { serializeTypeParameter } from "../TypeParameter.js";
4
- import { getReferencesForType, resolveTypeArgumentsInType, serializeType, validate, } from "../types/Type.js";
4
+ import { getReferencesForType, resolveTypeArgumentsInType, serializeType, setParent, validate, } from "../types/Type.js";
5
5
  import { getNestedDeclarations, getTypeArgumentsRecord, validateDeclName } from "./Declaration.js";
6
6
  export const GenTypeAliasDecl = (sourceUrl, options) => {
7
7
  validateDeclName(options.name);
@@ -9,11 +9,7 @@ export const GenTypeAliasDecl = (sourceUrl, options) => {
9
9
  ...options,
10
10
  kind: NodeKind.TypeAliasDecl,
11
11
  sourceUrl,
12
- type: Lazy.of(() => {
13
- const type = options.type(...options.parameters);
14
- type.parent = decl;
15
- return type;
16
- }),
12
+ type: Lazy.of(() => setParent(options.type(...options.parameters), decl)),
17
13
  };
18
14
  return decl;
19
15
  };
@@ -25,11 +21,7 @@ export const TypeAliasDecl = (sourceUrl, options) => {
25
21
  kind: NodeKind.TypeAliasDecl,
26
22
  sourceUrl,
27
23
  parameters: [],
28
- type: Lazy.of(() => {
29
- const type = options.type();
30
- type.parent = decl;
31
- return type;
32
- }),
24
+ type: Lazy.of(() => setParent(options.type(), decl)),
33
25
  };
34
26
  return decl;
35
27
  };
@@ -37,9 +37,15 @@ export type AsNode<T> = T extends (infer I)[] ? ArrayType<AsNode<I>> : T extends
37
37
  [K in keyof T]: T[K] extends MemberDecl ? T[K] : T extends null | undefined ? MemberDecl<AsNode<NonNullable<T[K]>>, false> : MemberDecl<AsNode<T[K]>, true>;
38
38
  }> : T extends string ? StringType : T extends number ? FloatType : T extends boolean ? BooleanType : never;
39
39
  export declare const getParentDecl: (type: Type) => Decl | undefined;
40
+ /**
41
+ * Sets the `parent` property of the passed `type` to the passed `parentNode`.
42
+ *
43
+ * The property is set on the instance. It does not create a new instance.
44
+ */
45
+ export declare const setParent: <T extends BaseType>(type: Omit<T, "parent">, parentNode: Type | Decl) => T;
46
+ export declare const removeParentKey: <T extends BaseType>(type: T) => Omit<T, "parent">;
40
47
  export declare const findTypeAtPath: (type: Type, path: string[]) => Type | undefined;
41
48
  export declare const serializeType: Serializer<Type, SerializedType>;
42
- export declare const removeParentKey: <T extends BaseType>(type: T) => Omit<T, "parent">;
43
49
  export declare const getReferencesForType: GetReferences<Type>;
44
50
  /**
45
51
  * Format the structure of a value to always look the same when serialized as JSON.
@@ -127,6 +127,20 @@ export const getParentDecl = (type) => {
127
127
  return getParentDecl(type.parent);
128
128
  }
129
129
  };
130
+ /**
131
+ * Sets the `parent` property of the passed `type` to the passed `parentNode`.
132
+ *
133
+ * The property is set on the instance. It does not create a new instance.
134
+ */
135
+ export const setParent = (type, parentNode) => {
136
+ ;
137
+ type.parent = parentNode;
138
+ return type;
139
+ };
140
+ export const removeParentKey = (type) => {
141
+ const { parent: _parent, ...rest } = type;
142
+ return rest;
143
+ };
130
144
  export const findTypeAtPath = (type, path) => {
131
145
  const [head, ...tail] = path;
132
146
  if (head === undefined) {
@@ -173,10 +187,6 @@ export const serializeType = type => {
173
187
  return assertExhaustive(type);
174
188
  }
175
189
  };
176
- export const removeParentKey = (type) => {
177
- const { parent: _parent, ...rest } = type;
178
- return rest;
179
- };
180
190
  export const getReferencesForType = (type, value) => {
181
191
  switch (type.kind) {
182
192
  case NodeKind.ArrayType:
@@ -4,7 +4,7 @@ import { wrapErrorsIfAny } from "../../../utils/error.js";
4
4
  import { getNestedDeclarations } from "../../declarations/Declaration.js";
5
5
  import { NodeKind } from "../../Node.js";
6
6
  import { validateOption } from "../../validation/options.js";
7
- import { formatValue, getReferencesForType, removeParentKey, resolveTypeArgumentsInType, serializeType, validate, } from "../Type.js";
7
+ import { formatValue, getReferencesForType, removeParentKey, resolveTypeArgumentsInType, serializeType, setParent, validate, } from "../Type.js";
8
8
  export const ArrayType = (items, options = {}) => {
9
9
  const type = {
10
10
  ...options,
@@ -13,7 +13,7 @@ export const ArrayType = (items, options = {}) => {
13
13
  maxItems: validateOption(options.maxItems, "maxItems", option => Number.isInteger(option) && option >= 0),
14
14
  items,
15
15
  };
16
- items.parent = type;
16
+ setParent(type.items, type);
17
17
  return type;
18
18
  };
19
19
  export { ArrayType as Array };
@@ -27,11 +27,15 @@ export const validateEnumType = (helpers, type, value) => {
27
27
  }
28
28
  const caseName = value[discriminatorKey];
29
29
  if (!(caseName in type.values)) {
30
- return [TypeError(`discriminator "${caseName}" is not a valid enum case`)];
30
+ return [
31
+ TypeError(`discriminator "${caseName}" is not a valid enum case, possible cases are: ${Object.keys(type.values).join(", ")}`),
32
+ ];
31
33
  }
32
34
  const unknownKeyErrors = actualKeys.flatMap(actualKey => actualKey === discriminatorKey || actualKey in type.values
33
35
  ? []
34
- : [TypeError(`key "${actualKey}" is not the discriminator key or a valid enum case`)]);
36
+ : [
37
+ TypeError(`key "${actualKey}" is not the discriminator key "${caseName}" or a valid enum case, possible cases are: ${Object.keys(type.values).join(", ")}`),
38
+ ]);
35
39
  if (unknownKeyErrors.length > 0) {
36
40
  return unknownKeyErrors;
37
41
  }
@@ -5,7 +5,7 @@ import { wrapErrorsIfAny } from "../../../utils/error.js";
5
5
  import { getNestedDeclarations } from "../../declarations/Declaration.js";
6
6
  import { NodeKind } from "../../Node.js";
7
7
  import { validateOption } from "../../validation/options.js";
8
- import { getReferencesForType, removeParentKey, resolveTypeArgumentsInType, serializeType, validate, } from "../Type.js";
8
+ import { formatValue, getReferencesForType, removeParentKey, resolveTypeArgumentsInType, serializeType, setParent, validate, } from "../Type.js";
9
9
  const keyPattern = /^[a-zA-Z0-9][a-zA-Z0-9_]*$/;
10
10
  export const ObjectType = (properties, options = {}) => {
11
11
  const type = {
@@ -20,7 +20,7 @@ export const ObjectType = (properties, options = {}) => {
20
20
  throw new TypeError(`Invalid object key "${key}". Object keys must not start with an underscore and may only contain letters, digits and underscores. (Pattern: ${keyPattern.source})`);
21
21
  }
22
22
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
23
- properties[key].type.parent = type;
23
+ setParent(properties[key].type, type);
24
24
  });
25
25
  return type;
26
26
  };
@@ -75,5 +75,8 @@ export const getReferencesForObjectType = (type, value) => typeof value === "obj
75
75
  key in type.properties ? getReferencesForType(type.properties[key].type, propValue) : [])
76
76
  : [];
77
77
  export const formatObjectValue = (type, value) => typeof value === "object" && value !== null && !Array.isArray(value)
78
- ? sortObjectKeys(value, Object.keys(type.properties))
78
+ ? sortObjectKeys(Object.fromEntries(Object.entries(value).map(([key, item]) => [
79
+ key,
80
+ type.properties[key] ? formatValue(type.properties[key].type, item) : item,
81
+ ])), Object.keys(type.properties))
79
82
  : value;
@@ -4,16 +4,12 @@ import { parallelizeErrors } from "../../../../shared/utils/validation.js";
4
4
  import { wrapErrorsIfAny } from "../../../utils/error.js";
5
5
  import { NodeKind } from "../../Node.js";
6
6
  import { getNestedDeclarationsInObjectType, getReferencesForObjectType, resolveTypeArgumentsInObjectType, serializeObjectType, validateObjectType, } from "../generic/ObjectType.js";
7
- import { removeParentKey } from "../Type.js";
7
+ import { formatValue, removeParentKey, setParent } from "../Type.js";
8
8
  export const NestedEntityMapType = (options) => {
9
9
  const nestedEntityMapType = {
10
10
  ...options,
11
11
  kind: NodeKind.NestedEntityMapType,
12
- type: Lazy.of(() => {
13
- const type = options.type;
14
- type.parent = nestedEntityMapType;
15
- return type;
16
- }),
12
+ type: Lazy.of(() => setParent(options.type, nestedEntityMapType)),
17
13
  };
18
14
  return nestedEntityMapType;
19
15
  };
@@ -22,11 +18,7 @@ const _NestedEntityMapType = (options) => {
22
18
  const nestedEntityMapType = {
23
19
  ...options,
24
20
  kind: NodeKind.NestedEntityMapType,
25
- type: Lazy.of(() => {
26
- const type = options.type();
27
- type.parent = nestedEntityMapType;
28
- return type;
29
- }),
21
+ type: Lazy.of(() => setParent(options.type(), nestedEntityMapType)),
30
22
  };
31
23
  return nestedEntityMapType;
32
24
  };
@@ -55,6 +47,6 @@ export const getReferencesForNestedEntityMapType = (type, value) => typeof value
55
47
  .flatMap(item => getReferencesForObjectType(type.type.value, item))
56
48
  .concat(Object.keys(value))
57
49
  : [];
58
- export const formatNestedEntityMapValue = (_type, value) => typeof value === "object" && value !== null && !Array.isArray(value)
59
- ? sortObjectKeysAlphabetically(value)
50
+ export const formatNestedEntityMapValue = (type, value) => typeof value === "object" && value !== null && !Array.isArray(value)
51
+ ? sortObjectKeysAlphabetically(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, formatValue(type.type.value, item)])))
60
52
  : value;
@@ -1,8 +1,9 @@
1
1
  import Debug from "debug";
2
2
  import express from "express";
3
+ import { getInstanceContainerOverview } from "../../../shared/utils/instances.js";
3
4
  import { isOk } from "../../../shared/utils/result.js";
4
5
  import { serializeDecl } from "../../schema/declarations/Declaration.js";
5
- import { isEntityDecl } from "../../schema/declarations/EntityDecl.js";
6
+ import { isEntityDecl, serializeEntityDecl } from "../../schema/declarations/EntityDecl.js";
6
7
  import { isEnumDecl } from "../../schema/declarations/EnumDecl.js";
7
8
  import { isTypeAliasDecl } from "../../schema/declarations/TypeAliasDecl.js";
8
9
  import { createInstance, deleteInstance, updateInstance } from "./instanceOperations.js";
@@ -59,8 +60,11 @@ declarationsApi.get("/:name/instances", (req, res) => {
59
60
  res.status(400).send(`Declaration "${decl.name}" is not an entity`);
60
61
  return;
61
62
  }
63
+ const serializedEntityDecl = serializeEntityDecl(decl);
62
64
  const body = {
63
- instances: req.instancesByEntityName[req.params.name] ?? [],
65
+ instances: req.instancesByEntityName[req.params.name]
66
+ ?.map(instanceContainer => getInstanceContainerOverview(serializedEntityDecl, instanceContainer, req.locales))
67
+ .toSorted((a, b) => a.displayName.localeCompare(b.displayName)) ?? [],
64
68
  isLocaleEntity: decl === req.localeEntity,
65
69
  };
66
70
  res.json(body);
@@ -17,7 +17,6 @@ gitApi.use((req, res, next) => {
17
17
  next();
18
18
  });
19
19
  gitApi.get("/status", async (req, res) => {
20
- const locales = (Array.isArray(req.query["locales"]) ? req.query["locales"] : [req.query["locales"]]).filter(locale => typeof locale === "string");
21
20
  const status = await req.git.status();
22
21
  attachGitStatusToInstancesByEntityName(req.instancesByEntityName, req.dataRoot,
23
22
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -31,7 +30,7 @@ gitApi.get("/status", async (req, res) => {
31
30
  .filter(instance => hasFileChanges(instance.gitStatus))
32
31
  .map(instance => getInstanceContainerOverview(
33
32
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
34
- serializeEntityDecl(req.entitiesByName[entityName]), instance, locales)),
33
+ serializeEntityDecl(req.entitiesByName[entityName]), instance, req.locales)),
35
34
  ])),
36
35
  };
37
36
  res.json(body);
@@ -4,10 +4,10 @@ import { v4 as uuidv4 } from "uuid";
4
4
  import { removeAt } from "../../../shared/utils/array.js";
5
5
  import { error, ok } from "../../../shared/utils/result.js";
6
6
  import { validateEntityDecl } from "../../schema/declarations/EntityDecl.js";
7
- import { formatValue } from "../../schema/index.js";
8
7
  import { createValidators } from "../../schema/Node.js";
9
8
  import { getErrorMessageForDisplay } from "../../utils/error.js";
10
9
  import { getGitFileStatusFromStatusResult } from "../../utils/git.js";
10
+ import { formatInstance } from "../../utils/instances.js";
11
11
  import { updateReferencesToInstances } from "../../utils/references.js";
12
12
  export const createInstance = async (locals, entityName, instance, idQueryParam) => {
13
13
  const entity = locals.entitiesByName[entityName];
@@ -27,7 +27,9 @@ export const createInstance = async (locals, entityName, instance, idQueryParam)
27
27
  return error([400, `Duplicate id "${id}" for locale entity`]);
28
28
  }
29
29
  const fileName = `${id}.json`;
30
- await writeFile(join(locals.dataRoot, entity.name, fileName), JSON.stringify(formatValue(entity.type.value, instance), undefined, 2), { encoding: "utf-8" });
30
+ await writeFile(join(locals.dataRoot, entity.name, fileName), formatInstance(entity, instance), {
31
+ encoding: "utf-8",
32
+ });
31
33
  const instanceContainer = {
32
34
  fileName,
33
35
  id,
@@ -56,7 +58,7 @@ export const updateInstance = async (locals, entityName, instanceId, instance) =
56
58
  if (validationErrors.length > 0) {
57
59
  return error([400, validationErrors.map(getErrorMessageForDisplay).join("\n\n")]);
58
60
  }
59
- await writeFile(join(locals.dataRoot, entity.name, instanceContainer.fileName), JSON.stringify(formatValue(entity.type.value, instance), undefined, 2), { encoding: "utf-8" });
61
+ await writeFile(join(locals.dataRoot, entity.name, instanceContainer.fileName), formatInstance(entity, instance), { encoding: "utf-8" });
60
62
  const oldInstance = instanceContainer.content;
61
63
  instanceContainer.content = instance;
62
64
  instanceContainer.gitStatus =
@@ -5,7 +5,6 @@ import type { Decl } from "../schema/declarations/Declaration.js";
5
5
  import type { EntityDecl } from "../schema/declarations/EntityDecl.js";
6
6
  import type { ReferencesToInstances } from "../utils/references.js";
7
7
  export type ServerOptions = {
8
- name: string;
9
8
  port: number;
10
9
  };
11
10
  export interface TSONDBRequestLocals {
@@ -18,6 +17,7 @@ export interface TSONDBRequestLocals {
18
17
  entitiesByName: Record<string, EntityDecl>;
19
18
  localeEntity?: EntityDecl;
20
19
  referencesToInstances: ReferencesToInstances;
20
+ locales: string[];
21
21
  }
22
22
  declare global {
23
23
  namespace Express {
@@ -1,23 +1,32 @@
1
1
  import Debug from "debug";
2
2
  import express from "express";
3
- import { join } from "node:path";
3
+ import { findPackageJSON } from "node:module";
4
+ import { dirname, join } from "node:path";
4
5
  import { api } from "./api/index.js";
5
6
  import { init } from "./init.js";
6
7
  const debug = Debug("tsondb:server");
7
8
  const defaultOptions = {
8
- name: "tsondb server",
9
9
  port: 3000,
10
10
  };
11
+ const staticNodeModule = (moduleName) => {
12
+ const pathToPackageJson = findPackageJSON(moduleName, import.meta.url);
13
+ if (!pathToPackageJson) {
14
+ throw new Error(`Could not find module "${moduleName}"`);
15
+ }
16
+ return express.static(dirname(pathToPackageJson));
17
+ };
11
18
  export const createServer = async (schema, dataRootPath, instancesByEntityName, options) => {
12
- const { name, port } = { ...defaultOptions, ...options };
19
+ const { port } = { ...defaultOptions, ...options };
13
20
  const app = express();
14
- app.use(express.static(join(import.meta.dirname, "../../public")));
15
- app.use("/js/node_modules", express.static(join(import.meta.dirname, "../../node_modules")));
16
- app.use("/js/client", express.static(join(import.meta.dirname, "../../lib/client")));
17
- app.use("/js/shared", express.static(join(import.meta.dirname, "../../lib/shared")));
21
+ app.use(express.static(join(import.meta.dirname, "../../../public")));
22
+ app.use("/js/node_modules/preact", staticNodeModule("preact"));
23
+ app.use("/js/node_modules/preact-iso", staticNodeModule("preact-iso"));
24
+ app.use("/js/client", express.static(join(import.meta.dirname, "../../../lib/web")));
25
+ app.use("/js/shared", express.static(join(import.meta.dirname, "../../../lib/shared")));
18
26
  app.use(express.json());
19
27
  const requestLocals = await init(schema, dataRootPath, Object.assign({}, instancesByEntityName));
20
28
  app.use((req, _res, next) => {
29
+ debug("%s %s", req.method, req.originalUrl);
21
30
  Object.assign(req, requestLocals);
22
31
  next();
23
32
  });
@@ -47,7 +56,17 @@ export const createServer = async (schema, dataRootPath, instancesByEntityName,
47
56
  </body>
48
57
  </html>`);
49
58
  });
50
- app.listen(port, () => {
51
- debug(`${name} listening on http://localhost:${port.toString()}`);
59
+ app.listen(port, (error) => {
60
+ if (error) {
61
+ if (error.code === "EADDRINUSE") {
62
+ debug(`port ${port.toString()} is already in use`);
63
+ }
64
+ else {
65
+ debug("error starting server:", error);
66
+ }
67
+ }
68
+ else {
69
+ debug(`server listening on http://localhost:${port.toString()}`);
70
+ }
52
71
  });
53
72
  };
@@ -39,6 +39,7 @@ export const init = async (schema, dataRootPath, instancesByEntityName) => {
39
39
  entitiesByName: entitiesByName,
40
40
  localeEntity: schema.localeEntity,
41
41
  referencesToInstances,
42
+ locales: ["de-DE", "en-US"], // TODO: Make this configurable
42
43
  };
43
44
  return requestLocals;
44
45
  };
@@ -3,3 +3,4 @@ import type { InstancesByEntityName } from "../../shared/utils/instances.js";
3
3
  import type { EntityDecl } from "../schema/declarations/EntityDecl.js";
4
4
  export declare const getInstancesByEntityName: (dataRoot: string, entities: readonly EntityDecl[]) => Promise<InstancesByEntityName>;
5
5
  export declare const attachGitStatusToInstancesByEntityName: (instancesByEntityName: InstancesByEntityName, dataRoot: string, gitRoot: string, gitStatus: StatusResult) => void;
6
+ export declare const formatInstance: (entity: EntityDecl, instanceContent: unknown) => string;
@@ -1,5 +1,6 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { basename, extname, join } from "node:path";
3
+ import { formatValue } from "../schema/index.js";
3
4
  import { getGitFileStatusFromStatusResult } from "./git.js";
4
5
  export const getInstancesByEntityName = async (dataRoot, entities) => Object.fromEntries(await Promise.all(entities.map(async (entity) => {
5
6
  const entityDir = join(dataRoot, entity.name);
@@ -19,3 +20,4 @@ export const attachGitStatusToInstancesByEntityName = (instancesByEntityName, da
19
20
  }));
20
21
  });
21
22
  };
23
+ export const formatInstance = (entity, instanceContent) => JSON.stringify(formatValue(entity.type.value, instanceContent), undefined, 2) + "\n";
@@ -13,7 +13,7 @@ export interface GetDeclarationResponseBody<D extends SerializedDecl = Serialize
13
13
  isLocaleEntity: boolean;
14
14
  }
15
15
  export interface GetAllInstancesOfEntityResponseBody {
16
- instances: InstanceContainer[];
16
+ instances: InstanceContainerOverview[];
17
17
  isLocaleEntity: boolean;
18
18
  }
19
19
  export interface CreateInstanceOfEntityResponseBody {
@@ -17,3 +17,4 @@ export interface ArrayDiffResult<T> {
17
17
  */
18
18
  removed: T[];
19
19
  }
20
+ export declare const unique: <T>(arr: T[], equalityCheck?: (a: T, b: T) => boolean) => T[];
@@ -28,3 +28,4 @@ export const difference = (oldArr, newArr) => newArr.reduce((acc, item) => {
28
28
  }
29
29
  return acc;
30
30
  }, { removed: oldArr, added: newArr });
31
+ export const unique = (arr, equalityCheck = (a, b) => a === b) => arr.filter((item, index) => arr.findIndex(other => equalityCheck(item, other)) === index);
@@ -12,6 +12,9 @@ const getValueAtPath = (value, path) => {
12
12
  return current;
13
13
  };
14
14
  export const getDisplayNameFromEntityInstance = (entity, instance, defaultName, locales = []) => {
15
+ if (entity.displayName === null) {
16
+ return defaultName;
17
+ }
15
18
  const displayNamePath = entity.displayName ?? "name";
16
19
  if (typeof displayNamePath === "string") {
17
20
  return getValueAtPath(instance, displayNamePath) ?? defaultName;
@@ -1,4 +1,7 @@
1
- export const sortObjectKeys = (obj, keys) => Object.fromEntries(keys.flatMap(key => (obj[key] === undefined ? [] : [[key, obj[key]]])));
1
+ export const sortObjectKeys = (obj, keys) => Object.fromEntries([
2
+ ...keys.flatMap(key => (obj[key] === undefined ? [] : [[key, obj[key]]])),
3
+ ...Object.entries(obj).filter(([key]) => !keys.includes(key)),
4
+ ]);
2
5
  export const sortObjectKeysAlphabetically = (obj) => Object.fromEntries(Object.entries(obj).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)));
3
6
  export const mergeObjects = (obj1, obj2, solveConflict) => Object.entries(obj2).reduce((acc, [key, value]) => ({
4
7
  ...acc,
package/lib/web/api.d.ts CHANGED
@@ -9,7 +9,7 @@ export declare const getInstanceByEntityNameAndId: (name: string, id: string) =>
9
9
  export declare const updateInstanceByEntityNameAndId: (name: string, id: string, content: unknown) => Promise<UpdateInstanceOfEntityResponseBody>;
10
10
  export declare const deleteInstanceByEntityNameAndId: (name: string, id: string) => Promise<DeleteInstanceOfEntityResponseBody>;
11
11
  export declare const getAllInstances: (locales: string[]) => Promise<GetAllInstancesResponseBody>;
12
- export declare const getGitStatus: (locales: string[]) => Promise<GitStatusResponseBody>;
12
+ export declare const getGitStatus: () => Promise<GitStatusResponseBody>;
13
13
  export declare const stageAllFiles: () => Promise<void>;
14
14
  export declare const stageAllFilesOfEntity: (entityName: string) => Promise<void>;
15
15
  export declare const stageFileOfEntity: (entityName: string, id: string) => Promise<void>;
package/lib/web/api.js CHANGED
@@ -81,11 +81,8 @@ export const getAllInstances = async (locales) => {
81
81
  }
82
82
  return response.json();
83
83
  };
84
- export const getGitStatus = async (locales) => {
84
+ export const getGitStatus = async () => {
85
85
  const url = new URL("/api/git/status", window.location.origin);
86
- for (const locale of locales) {
87
- url.searchParams.append("locales", locale);
88
- }
89
86
  const response = await fetch(url);
90
87
  if (!response.ok) {
91
88
  throw new Error(await response.text());
@@ -28,7 +28,7 @@ export const Git = () => {
28
28
  const [entities, setEntities] = useState([]);
29
29
  const [allBranches, setAllBranches] = useState([]);
30
30
  const [currentBranch, setCurrentBranch] = useState("");
31
- const updateGitStatus = (localEntities) => Promise.all([getGitStatus(["de-DE"]), getBranches()]).then(([statusData, branchesData]) => {
31
+ const updateGitStatus = (localEntities) => Promise.all([getGitStatus(), getBranches()]).then(([statusData, branchesData]) => {
32
32
  setIndexFiles(filterFilesForDisplay(isChangedInIndex, localEntities, statusData));
33
33
  setWorkingTreeFiles(filterFilesForDisplay(isChangedInWorkingDir, localEntities, statusData));
34
34
  setCommitsAhead(statusData.commitsAhead);
@@ -1,2 +1,3 @@
1
1
  import { useMappedAPIResource } from "./useMappedAPIResource.js";
2
- export const useAPIResource = (apiFn, ...args) => useMappedAPIResource(apiFn, data => data, ...args);
2
+ const identity = (data) => data;
3
+ export const useAPIResource = (apiFn, ...args) => useMappedAPIResource(apiFn, identity, ...args);
@@ -12,7 +12,8 @@ export const useInstanceNamesByEntity = (locales = []) => {
12
12
  console.error("Error fetching data:", error.toString());
13
13
  }
14
14
  });
15
- }, [locales]);
15
+ // eslint-disable-next-line react-hooks/exhaustive-deps
16
+ }, [locales.toString()]);
16
17
  useEffect(() => {
17
18
  updateInstanceNamesByEntity();
18
19
  }, [updateInstanceNamesByEntity]);
@@ -3,7 +3,9 @@ export const useMappedAPIResource = (apiFn, mapFn, ...args) => {
3
3
  const [data, setData] = useState();
4
4
  const fetchData = useCallback(() => apiFn(...args).then(result => {
5
5
  setData(mapFn(result));
6
- }), [apiFn, args, mapFn]);
6
+ }),
7
+ // eslint-disable-next-line react-hooks/exhaustive-deps
8
+ [apiFn, mapFn, ...args]);
7
9
  useEffect(() => {
8
10
  fetchData().catch((err) => {
9
11
  console.log(err);
@@ -1,17 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
2
  import { useRoute } from "preact-iso";
3
3
  import { useEffect } from "preact/hooks";
4
- import { getDisplayNameFromEntityInstance } from "../../shared/utils/displayName.js";
5
4
  import { getGitStatusForDisplay, getLabelForGitStatus } from "../../shared/utils/git.js";
6
5
  import { deleteInstanceByEntityNameAndId, getEntityByName, getInstancesByEntityName, } from "../api.js";
7
6
  import { Layout } from "../components/Layout.js";
8
7
  import { useAPIResource } from "../hooks/useAPIResource.js";
9
8
  import { useMappedAPIResource } from "../hooks/useMappedAPIResource.js";
10
9
  import { NotFound } from "./NotFound.js";
10
+ const mapInstances = (data) => data.instances;
11
11
  export const Entity = () => {
12
12
  const { params: { name }, query: { created }, } = useRoute();
13
13
  const [entity] = useAPIResource(getEntityByName, name ?? "");
14
- const [instances, reloadInstances] = useMappedAPIResource(getInstancesByEntityName, data => data.instances, name ?? "");
14
+ const [instances, reloadInstances] = useMappedAPIResource(getInstancesByEntityName, mapInstances, name ?? "");
15
15
  useEffect(() => {
16
16
  if (created) {
17
17
  const instanceElement = document.getElementById(`instance-${created}`);
@@ -28,7 +28,7 @@ export const Entity = () => {
28
28
  }
29
29
  return (_jsxs(Layout, { breadcrumbs: [{ url: "/", label: "Home" }], children: [_jsxs("div", { class: "header-with-btns", children: [_jsx("h1", { children: name }), _jsx("a", { class: "btn btn--primary", href: `/entities/${entity.declaration.name}/instances/create`, children: "Add" })] }), entity.declaration.comment && _jsx("p", { className: "description", children: entity.declaration.comment }), _jsxs("p", { children: [instances.length, " instance", instances.length === 1 ? "" : "s"] }), _jsx("ul", { class: "instances", children: instances.map(instance => {
30
30
  const gitStatusForDisplay = getGitStatusForDisplay(instance.gitStatus);
31
- return (_jsxs("li", { id: `instance-${instance.id}`, class: `instance-item ${created === instance.id ? "instance-item--created" : ""} ${gitStatusForDisplay === undefined ? "" : `git-status--${gitStatusForDisplay}`}`, children: [_jsx("h2", { children: getDisplayNameFromEntityInstance(entity.declaration, instance.content, instance.id) }), _jsx("p", { "aria-hidden": true, class: "id", children: instance.id }), gitStatusForDisplay !== undefined && (_jsx("p", { class: `git-status git-status--${gitStatusForDisplay}`, title: getLabelForGitStatus(gitStatusForDisplay), children: gitStatusForDisplay })), _jsxs("div", { className: "btns", children: [_jsx("a", { href: `/entities/${entity.declaration.name}/instances/${instance.id}`, class: "btn", children: "Edit" }), _jsx("button", { class: "destructive", onClick: () => {
31
+ return (_jsxs("li", { id: `instance-${instance.id}`, class: `instance-item ${created === instance.id ? "instance-item--created" : ""} ${gitStatusForDisplay === undefined ? "" : `git-status--${gitStatusForDisplay}`}`, children: [_jsx("h2", { children: instance.displayName }), _jsx("p", { "aria-hidden": true, class: "id", children: instance.id }), gitStatusForDisplay !== undefined && (_jsx("p", { class: `git-status git-status--${gitStatusForDisplay}`, title: getLabelForGitStatus(gitStatusForDisplay), children: gitStatusForDisplay })), _jsxs("div", { className: "btns", children: [_jsx("a", { href: `/entities/${entity.declaration.name}/instances/${instance.id}`, class: "btn", children: "Edit" }), _jsx("button", { class: "destructive", onClick: () => {
32
32
  if (confirm("Are you sure you want to delete this instance?")) {
33
33
  deleteInstanceByEntityNameAndId(entity.declaration.name, instance.id)
34
34
  .then(() => reloadInstances())
@@ -3,7 +3,8 @@ import { toTitleCase } from "../../shared/utils/string.js";
3
3
  import { getAllEntities } from "../api.js";
4
4
  import { Layout } from "../components/Layout.js";
5
5
  import { useMappedAPIResource } from "../hooks/useMappedAPIResource.js";
6
+ const mapEntities = (data) => data.declarations.sort((a, b) => a.declaration.name.localeCompare(b.declaration.name));
6
7
  export const Home = () => {
7
- const [entities] = useMappedAPIResource(getAllEntities, data => data.declarations.sort((a, b) => a.declaration.name.localeCompare(b.declaration.name)));
8
+ const [entities] = useMappedAPIResource(getAllEntities, mapEntities);
8
9
  return (_jsxs(Layout, { breadcrumbs: [{ url: "/", label: "Home" }], children: [_jsx("h1", { children: "Entities" }), _jsx("ul", { class: "entities", children: (entities ?? []).map(entity => (_jsxs("li", { class: "entity-item", children: [_jsxs("div", { className: "title", children: [_jsx("h2", { children: toTitleCase(entity.declaration.name) }), entity.declaration.comment && _jsx("p", { children: entity.declaration.comment })] }), _jsxs("p", { class: "meta", children: [entity.instanceCount, " instance", entity.instanceCount === 1 ? "" : "s"] }), _jsx("div", { className: "btns", children: _jsx("a", { href: `/entities/${entity.declaration.name}`, class: "btn", children: "View" }) })] }, entity.declaration.name))) })] }));
9
10
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsondb",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",
@@ -28,7 +28,8 @@
28
28
  "lint": "eslint",
29
29
  "check-format": "prettier \"{src,test}/**/*.{ts,tsx}\" --check",
30
30
  "format": "prettier \"{src,test}/**/*.{ts,tsx}\" --write",
31
- "release": "commit-and-tag-version"
31
+ "release": "commit-and-tag-version",
32
+ "release:sign": "commit-and-tag-version --sign --signoff"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@eslint/js": "^9.30.1",
@@ -15,6 +15,7 @@
15
15
  --separator-color: #e9e5eb;
16
16
  --secondary-color: #7e7f87;
17
17
  --tertiary-color: #bcbdc2;
18
+ --input-border-color: #black;
18
19
  --button-background: #c7cad0;
19
20
  --button-background-hover: #dedfe4;
20
21
  --button-background-primary: black;
@@ -32,10 +33,51 @@
32
33
  --highlight-color: #a08500;
33
34
  --highlight-background: #fdfab9;
34
35
  --disabled-color: #cdced2;
36
+ --git-status-untracked-color: rgb(13, 149, 101);
37
+ --git-status-untracked-background: rgba(13, 149, 101, 0.15);
38
+ --git-status-added-color: rgb(20, 148, 29);
39
+ --git-status-added-background: rgba(20, 148, 29, 0.15);
40
+ --git-status-modified-color: rgb(196, 155, 18);
41
+ --git-status-modified-background: rgba(196, 155, 18, 0.15);
42
+ --git-status-deleted-color: rgb(135, 22, 11);
43
+ --git-status-deleted-background: rgba(135, 22, 11, 0.15);
44
+ --git-status-renamed-color: rgb(20, 97, 148);
45
+ --git-status-renamed-background: rgba(20, 97, 148, 0.15);
46
+ --shadow-color: rgba(160, 163, 165, 0.5);
47
+ }
48
+
49
+ @media (prefers-color-scheme: dark) {
50
+ :root {
51
+ --background: hsl(260, 6%, 10%);
52
+ --markdown-background: hsl(260, 6%, 17%);
53
+ --color: hsl(260, 6%, 88%);
54
+ --error-color: #ff6b6b;
55
+ --separator-color: hsl(260, 6%, 24%);
56
+ --secondary-color: hsl(260, 6%, 69%);
57
+ --tertiary-color: hsl(260, 6%, 49%);
58
+ --input-border-color: hsl(260, 6%, 40%);
59
+ --button-background: hsl(260, 6%, 24%);
60
+ --button-background-hover: hsl(260, 6%, 30%);
61
+ --button-background-primary: hsl(260, 6%, 80%);
62
+ --button-background-primary-hover: hsl(260, 6%, 60%);
63
+ --button-background-destructive: #922727;
64
+ --button-background-destructive-hover: #ff8080;
65
+ --button-background-disabled: hsl(260, 6%, 35%);
66
+ --button-color: #fff;
67
+ --button-color-hover: #fff;
68
+ --button-color-primary: #000;
69
+ --button-color-primary-hover: #000;
70
+ --button-color-destructive: #fff;
71
+ --button-color-destructive-hover: #fff;
72
+ --button-color-disabled: hsl(260, 6%, 54%);
73
+ --highlight-color: #f2d600;
74
+ --highlight-background: #333300;
75
+ }
35
76
  }
36
77
 
37
78
  body {
38
79
  font-family: var(--font-sans);
80
+ color: var(--color);
39
81
  margin: 0;
40
82
  padding: 1.5rem;
41
83
  line-height: 1.4;
@@ -143,6 +185,13 @@ button.primary {
143
185
  border: none;
144
186
  }
145
187
 
188
+ @media (prefers-color-scheme: dark) {
189
+ a.btn--primary,
190
+ button.primary {
191
+ font-weight: 700;
192
+ }
193
+ }
194
+
146
195
  a.btn--primary:hover,
147
196
  button.primary:hover {
148
197
  background: var(--button-background-primary-hover);
@@ -179,8 +228,10 @@ button.destructive:disabled {
179
228
  input,
180
229
  textarea,
181
230
  select {
231
+ background: var(--background);
232
+ color: var(--color);
182
233
  font-family: var(--font-sans);
183
- border: 1px solid var(--color);
234
+ border: 1px solid var(--input-border-color);
184
235
  padding: 0.5rem;
185
236
  display: block;
186
237
  width: 100%;
@@ -301,7 +352,7 @@ select {
301
352
  font-size: 1rem;
302
353
  margin: 0;
303
354
  flex: 1 1 auto;
304
- padding: 0.5rem 0;
355
+ padding: 0.65rem 0;
305
356
  }
306
357
 
307
358
  .entity-item .title {
@@ -507,28 +558,28 @@ button[type="submit"] {
507
558
  }
508
559
 
509
560
  .git-status.git-status--U {
510
- color: rgb(13, 149, 101);
511
- background: rgba(13, 149, 101, 0.15);
561
+ color: var(--git-status-untracked-color);
562
+ background: var(--git-status-untracked-background);
512
563
  }
513
564
 
514
565
  .git-status.git-status--A {
515
- color: rgb(20, 148, 29);
516
- background: rgba(20, 148, 29, 0.15);
566
+ color: var(--git-status-added-color);
567
+ background: var(--git-status-added-background);
517
568
  }
518
569
 
519
570
  .git-status.git-status--M {
520
- color: rgb(196, 155, 18);
521
- background: rgba(196, 155, 18, 0.15);
571
+ color: var(--git-status-modified-color);
572
+ background: var(--git-status-modified-background);
522
573
  }
523
574
 
524
575
  .git-status.git-status--D {
525
- color: rgb(135, 22, 11);
526
- background: rgba(135, 22, 11, 0.15);
576
+ color: var(--git-status-deleted-color);
577
+ background: var(--git-status-deleted-background);
527
578
  }
528
579
 
529
580
  .git-status.git-status--R {
530
- color: rgb(20, 97, 148);
531
- background: rgba(20, 97, 148, 0.15);
581
+ color: var(--git-status-renamed-color);
582
+ background: var(--git-status-renamed-background);
532
583
  }
533
584
 
534
585
  aside.git {
@@ -632,7 +683,7 @@ aside.git .branch .select-wrapper {
632
683
  display: none;
633
684
  position: absolute;
634
685
  right: 1.5rem;
635
- box-shadow: 0 0.5rem 2rem rgba(160, 163, 165, 0.5);
686
+ box-shadow: 0 0.5rem 2rem var(--shadow-color);
636
687
  z-index: 1000;
637
688
  padding: 1rem;
638
689
  background: var(--background);