gruber 0.2.0 → 0.4.0

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 (73) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +156 -16
  3. package/core/configuration.d.ts +157 -0
  4. package/core/configuration.js +222 -96
  5. package/core/configuration.test.d.ts +1 -0
  6. package/core/configuration.test.js +242 -53
  7. package/{types/core → core}/fetch-router.d.ts +0 -1
  8. package/core/fetch-router.test.d.ts +1 -0
  9. package/{types/core → core}/http.d.ts +30 -12
  10. package/core/http.js +42 -17
  11. package/core/http.test.d.ts +1 -0
  12. package/core/http.test.js +57 -35
  13. package/{types/core → core}/migrator.d.ts +0 -1
  14. package/core/migrator.test.d.ts +1 -0
  15. package/{types/core → core}/mod.d.ts +1 -1
  16. package/core/mod.js +1 -0
  17. package/{types/core → core}/postgres.d.ts +0 -1
  18. package/core/structures.d.ts +91 -0
  19. package/core/structures.js +260 -0
  20. package/core/structures.test.d.ts +1 -0
  21. package/core/structures.test.js +474 -0
  22. package/core/test-deps.d.ts +1 -0
  23. package/core/test-deps.js +1 -1
  24. package/{types/core → core}/types.d.ts +0 -1
  25. package/{types/core → core}/utilities.d.ts +0 -1
  26. package/core/utilities.test.d.ts +1 -0
  27. package/package.json +4 -5
  28. package/{types/source → source}/configuration.d.ts +4 -9
  29. package/source/configuration.js +0 -2
  30. package/source/core.d.ts +1 -0
  31. package/{types/source → source}/express-router.d.ts +2 -3
  32. package/source/express-router.js +1 -1
  33. package/{types/source → source}/koa-router.d.ts +0 -1
  34. package/{types/source → source}/mod.d.ts +0 -3
  35. package/source/mod.js +2 -2
  36. package/{types/source → source}/node-router.d.ts +8 -8
  37. package/source/node-router.js +1 -1
  38. package/source/package-lock.json +3 -9
  39. package/source/package.json +0 -2
  40. package/source/polyfill.d.ts +1 -0
  41. package/{types/source → source}/postgres.d.ts +0 -1
  42. package/tsconfig.json +0 -2
  43. package/types/core/configuration.d.ts +0 -57
  44. package/types/core/configuration.d.ts.map +0 -1
  45. package/types/core/configuration.test.d.ts +0 -2
  46. package/types/core/configuration.test.d.ts.map +0 -1
  47. package/types/core/fetch-router.d.ts.map +0 -1
  48. package/types/core/fetch-router.test.d.ts +0 -2
  49. package/types/core/fetch-router.test.d.ts.map +0 -1
  50. package/types/core/http.d.ts.map +0 -1
  51. package/types/core/http.test.d.ts +0 -2
  52. package/types/core/http.test.d.ts.map +0 -1
  53. package/types/core/migrator.d.ts.map +0 -1
  54. package/types/core/migrator.test.d.ts +0 -2
  55. package/types/core/migrator.test.d.ts.map +0 -1
  56. package/types/core/mod.d.ts.map +0 -1
  57. package/types/core/postgres.d.ts.map +0 -1
  58. package/types/core/test-deps.d.ts +0 -2
  59. package/types/core/test-deps.d.ts.map +0 -1
  60. package/types/core/types.d.ts.map +0 -1
  61. package/types/core/utilities.d.ts.map +0 -1
  62. package/types/core/utilities.test.d.ts +0 -2
  63. package/types/core/utilities.test.d.ts.map +0 -1
  64. package/types/source/configuration.d.ts.map +0 -1
  65. package/types/source/core.d.ts +0 -2
  66. package/types/source/core.d.ts.map +0 -1
  67. package/types/source/express-router.d.ts.map +0 -1
  68. package/types/source/koa-router.d.ts.map +0 -1
  69. package/types/source/mod.d.ts.map +0 -1
  70. package/types/source/node-router.d.ts.map +0 -1
  71. package/types/source/polyfill.d.ts +0 -2
  72. package/types/source/polyfill.d.ts.map +0 -1
  73. package/types/source/postgres.d.ts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  This file documents notable changes to the project
4
4
 
5
+ ## 0.4.0
6
+
7
+ **new**
8
+
9
+ - Added `config.number(...)` & `config.boolean(...)` types along with `Structure` equivolents.
10
+ - Set a response body when creating a `HTTPError`, either via the constructor or the static methods.
11
+ - Set headers when creating an `HTTPError` and mutate the headers on it too, to be passed to the Response.
12
+ - Structure primatives' fallback is now optional. If a fallback isn't provided, validation will fail if with a "Missing value" if no value is provided.
13
+ - Added an unstable/experimental `Structure.array` for validating an array of a single Structure, e.g. an array of strings.
14
+ - Add number and boolean configurations (and their structures)
15
+
16
+ **fixes**
17
+
18
+ - Improve JSDoc types for Deno / Node clients
19
+ - Fix Structure typings
20
+ - Organise Config/Structure/Spec wording
21
+
22
+ ## 0.3.0
23
+
24
+ **new**
25
+
26
+ - Removed use of superstruct in favour of new `structures.js` implementation
27
+ - Added `getJSONSchema` method to `Configuration`
28
+
29
+ **fixed**
30
+
31
+ - Node.js types should work now
32
+ - Node.js types includes a url-pattern polyfil
33
+
5
34
  ## 0.2.0
6
35
 
7
36
  **new**
package/README.md CHANGED
@@ -235,17 +235,20 @@ Building on the [HTTP server](#http-server) above, we'll setup configuration. St
235
235
  **config.js**
236
236
 
237
237
  ```js
238
- import superstruct from "superstruct";
238
+ import fs from "node:fs";
239
239
  import { getNodeConfiguration } from "gruber";
240
240
 
241
241
  const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
242
- const config = getNodeConfiguration({ superstruct });
242
+ const config = getNodeConfiguration();
243
243
 
244
- export function getSpecification() {
244
+ export function getConfigStruct() {
245
245
  return config.object({
246
- env: config.string({
247
- variable: "NODE_ENV",
248
- fallback: "development",
246
+ env: config.string({ variable: "NODE_ENV", fallback: "development" }),
247
+
248
+ port: config.number({
249
+ variable: "APP_PORT",
250
+ flag: "--port",
251
+ fallback: 8000,
249
252
  }),
250
253
 
251
254
  selfUrl: config.url({
@@ -253,13 +256,13 @@ export function getSpecification() {
253
256
  fallback: "http://localhost:3000",
254
257
  }),
255
258
 
256
- // Short hands?
257
259
  meta: config.object({
258
260
  name: config.string({ flag: "--app-name", fallback: pkg.name }),
259
261
  version: config.string({ fallback: pkg.version }),
260
262
  }),
261
263
 
262
264
  database: config.object({
265
+ useSsl: config.boolean({ flag: "--database-ssl", fallback: true }),
263
266
  url: config.url({
264
267
  variable: "DATABASE_URL",
265
268
  flag: "--database-url",
@@ -271,11 +274,11 @@ export function getSpecification() {
271
274
 
272
275
  // Load the configuration and parse it
273
276
  export function loadConfiguration(path) {
274
- return config.load(path, getSpecification());
277
+ return config.load(path, getConfigStruct());
275
278
  }
276
279
 
277
280
  // TypeScript thought:
278
- // export type Configuration = Infer<ReturnType<typeof getSpecification>>
281
+ // export type Configuration = Infer<ReturnType<typeof getConfigStruct>>
279
282
 
280
283
  // Expose the configutation for use in the application
281
284
  export const appConfig = await loadConfiguration(
@@ -284,7 +287,12 @@ export const appConfig = await loadConfiguration(
284
287
 
285
288
  // Export a method to generate usage documentation
286
289
  export function getConfigurationUsage() {
287
- return config.getUsage(getSpecification());
290
+ return config.getUsage(getConfigStruct());
291
+ }
292
+
293
+ // Export a method to generate a JSON Schema for the configuration
294
+ export function getConfigurationSchema() {
295
+ return config.getJSONSchema(getConfigStruct());
288
296
  }
289
297
  ```
290
298
 
@@ -357,7 +365,7 @@ this can be done like so:
357
365
 
358
366
  ```js
359
367
  export function loadConfiguration() {
360
- const appConfig = config.loadJsonSync(path, getSpecification());
368
+ const appConfig = config.loadJsonSync(path, getConfigStruct());
361
369
 
362
370
  // Only run these checks when running in production
363
371
  if (appConfig.env === "production") {
@@ -672,7 +680,6 @@ To see how it works, look at the [Node](./node/source/configuration.js) and [Den
672
680
  You can use the static `getOptions` method both subclasses provide and override the parts you want.
673
681
  These are the options:
674
682
 
675
- - `superstruct` — Configuration is based on [superstruct](https://docs.superstructjs.org/), you can pass your own instance if you like.
676
683
  - `readTextFile(url)` — How to load a text file from the file system
677
684
  - `getEnvironmentVariable(key)` — Return a matching environment "variable" for a key
678
685
  - `getCommandArgument(key)` — Get the corresponding "flag" from a CLI argument
@@ -684,10 +691,9 @@ For example, to override in Node:
684
691
  ```js
685
692
  import { Configuration, getNodeConfigOptions } from "gruber";
686
693
  import Yaml from "yaml";
687
- import superstruct from "superstruct";
688
694
 
689
695
  const config = new Configuration({
690
- ...getNodeConfigOptions({ superstruct }),
696
+ ...getNodeConfigOptions(),
691
697
  getEnvionmentVariable: () => undefined,
692
698
  stringify: (v) => Yaml.stringify(v),
693
699
  parse: (v) => Yaml.parse(v),
@@ -805,6 +811,8 @@ more can be added in the future as the need arrises.
805
811
  They directly map to HTTP error as codes documented on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
806
812
 
807
813
  ```js
814
+ import { HTTPError } from "gruber";
815
+
808
816
  const teapot = new HTTPError(418, "I'm a teapot");
809
817
  ```
810
818
 
@@ -818,6 +826,55 @@ teapot.toResponse();
818
826
  Currently, you can't set the body of the generated Response objects.
819
827
  This would be nice to have in the future, but the API should be thoughtfully designed first.
820
828
 
829
+ **Request body**
830
+
831
+ You can set the body to be returned when the HTTPError is thrown from the constructor or the factory methods:
832
+
833
+ ```ts
834
+ import { HTTPError } from "gruber";
835
+
836
+ const teapot = new HTTPError(418, "I'm a teapot", "model=teabot-5000");
837
+
838
+ throw HTTPError.badRequest("no coffee provided");
839
+ ```
840
+
841
+ The value of the body is the same as the `body` in the
842
+ [Response constructor](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#body).
843
+
844
+ **Headers**
845
+
846
+ > _EXPERIMENTAL_
847
+
848
+ If you really want, you can set headers on a HTTPError too:
849
+
850
+ ```ts
851
+ import { HTTPError } from "gruber";
852
+
853
+ const teapot = new HTTPError(
854
+ 400,
855
+ "Bad Request",
856
+ JSON.stringify({ some: "thing" }),
857
+ { "Content-Type": "application/json" },
858
+ );
859
+
860
+ // or via mutating the headers object
861
+ teapot.headers.set("X-HOTEL-BAR", "Hotel Bar?");
862
+ ```
863
+
864
+ If you want fine-grain control, you might be better off creating a subclass, e.g. `BadJSONRequest`:
865
+
866
+ ```ts
867
+ class BadJSONRequest extends HTTPError {
868
+ constructor(body) {
869
+ super(400, "Bad Request", body, { "Content-type": "application/json" });
870
+ this.name = "BadJSONRequest";
871
+ Error.captureStackTrace(this, BadJSONRequest);
872
+ }
873
+ }
874
+
875
+ throw new BadJSONRequest({ message: "Something went wrong..." });
876
+ ```
877
+
821
878
  ### FetchRouter
822
879
 
823
880
  `FetchRouter` is a web-native router for routes defined with `defineRoute`.
@@ -870,7 +927,7 @@ You can use it and override parts of it to customise how the postgres migrator w
870
927
 
871
928
  ### Utilities
872
929
 
873
- ### loader
930
+ #### loader
874
931
 
875
932
  `loader` let's you memoize the result of a function to create a singleton from it.
876
933
  It works synchronously or with promises.
@@ -914,6 +971,89 @@ This will generate the table:
914
971
  | Tyler Rockwell | ~ |
915
972
  ```
916
973
 
974
+ #### Structure
975
+
976
+ This is an internal primative for validating objects, strings, numbers and URLs for use in [Configuration](#configuration).
977
+ It is based on a very specific use of [superstruct](https://github.com/ianstormtaylor/superstruct) which it made sense to internalise to make the code base more portable.
978
+ A `Structure` is a type that validates a value is correct by throwing an error if validation fails, i.e. the wrong type is passed.
979
+ Every struct has an intrinsic `fallback` so that if no value (`undefined`) is passed, that is used instead.
980
+
981
+ ```js
982
+ import { Structure } from "gruber/structures.js";
983
+
984
+ // A string primative, or use "Geoff Testington" if no value is passed.
985
+ const name = Structure.string("Geoff Testington");
986
+
987
+ // A URL instance or a string that contains a valid URL, always converting to a URL
988
+ const website = Structure.url("https://example.com");
989
+
990
+ // A number primative, falling back to 42
991
+ const age = Structure.number(42);
992
+
993
+ // An object with all of the fields above and nothing else
994
+ // defaulting to create { name: "Geoff..", age: 42, website: "https..." } with the same fallback values
995
+ const person = Structure.object({ name, age, website });
996
+
997
+ // Process the Structure and get a value out. The returned value is strongly typed!
998
+ // This will throw if the value passed does not match the schema.
999
+ const value = person.process(/* ... */);
1000
+ ```
1001
+
1002
+ Those static Structure methods return a `Structure` instance. You can also create your own types with the constructor. This example shows how to do that, and also starts to unveil how the internals work a bit with [StructError](#structerror).
1003
+
1004
+ ```js
1005
+ import { Structure, StructError } from "gruber/structures.js";
1006
+
1007
+ // Create a new boolean structure (this should probably be included as Structure.boolean tbh)
1008
+ const boolean = new Structure(
1009
+ { type: "boolean", default: false },
1010
+ (input, context) => {
1011
+ if (input === undefined) return false;
1012
+ if (typeof input !== "boolean") {
1013
+ throw new StructError("Expected a boolean", context?.path);
1014
+ }
1015
+ return input;
1016
+ },
1017
+ );
1018
+ ```
1019
+
1020
+ To create a custom Structure, you give it a [JSON schema](https://json-schema.org/) and a "process" function.
1021
+ The function is called to validate a value against the structure. It should return the processed value or throw a `StructError`.
1022
+
1023
+ The `context` object might not be set and this means the struct is at the root level. If it is nested in an `object` then the context contains the path that the struct is located at, all the way from the root object. That path is expressed as an array of strings. That path is used to generate friendlier error messages to explain which nested field failed.
1024
+
1025
+ With a Structure, you can generate a JSON Schema:
1026
+
1027
+ ```js
1028
+ import { Structure } from "gruber/structures.js";
1029
+
1030
+ const person = Structure.object({
1031
+ name: Structure.string("Geoff Testington"),
1032
+ age: Structure.number(42),
1033
+ website: Structure.url("https://example.com"),
1034
+ });
1035
+
1036
+ console.log(JSON.stringify(person.getSchema(), null, 2));
1037
+ ```
1038
+
1039
+ This is a bit WIP, but you could use this to generate a JSON schema to lint configurations in your IDE.
1040
+
1041
+ #### StructError
1042
+
1043
+ This Error subclass contains extra information about why parsing a `Structure` failed.
1044
+
1045
+ - The `message` field is a description of what went wrong, in the context of the structure.
1046
+ - An extra `path` field exists to describe the path from the root object to get to this failed structure
1047
+ - `children` is also available to let a structure have multiple child errors, i.e. for an object to have failures for each of the fields that have failed.
1048
+
1049
+ On the error, there are also methods to help use it:
1050
+
1051
+ - `toFriendlyString` goes through all nested failures and outputs a single message to describe everything that went wrong.
1052
+ - `getOneLiner` converts the error to a succint one-line error message, concatentating the path and message
1053
+ - `[Symbol.iterator]` is also available if you want to loop through all children nodes, only those that do not have children themselves.
1054
+
1055
+ There is also the static method `StructError.chain(error, context)` which is useful for catching errors and applying a context to them (if they are not already a StructError).
1056
+
917
1057
  ## Node.js library
918
1058
 
919
1059
  There are some specific helpers to help use Gruber in Node.js apps.
@@ -969,7 +1109,7 @@ For older version of Node.js that don't support the latest web-standards,
969
1109
  there is a polyfil import you can use to add support for them to your runtime.
970
1110
 
971
1111
  ```js
972
- import "gruber/polyfil";
1112
+ import "gruber/polyfill.js";
973
1113
  ```
974
1114
 
975
1115
  This currently polyfils these APIs:
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @template [T=any]
3
+ * @typedef {object} SpecOptions
4
+ * @property {string} [variable]
5
+ * @property {string} [flag]
6
+ * @property {T} fallback
7
+ */
8
+ /**
9
+ * @template T
10
+ * @typedef {object} ConfigurationResult
11
+ * @property {'argument' | 'variable' | 'fallback'} source
12
+ * @property {string|T} value
13
+ */
14
+ /**
15
+ * @typedef {object} ConfigurationDescription
16
+ * @property {unknown} fallback
17
+ * @property {Record<string,string>[]} fields
18
+ */
19
+ /**
20
+ * @typedef {object} Specification
21
+ * @property {string} type
22
+ * @property {any} options
23
+ * @property {(name: string) => ConfigurationDescription} describe
24
+ */
25
+ /**
26
+ * @param {string} name
27
+ * @param {unknown} value
28
+ * @returns {Specification}
29
+ */
30
+ export function _getSpec(name: string, value: unknown): Specification;
31
+ export class _ObjectSpec {
32
+ /** @param {Record<string, SpecOptions>} options */
33
+ constructor(options: Record<string, SpecOptions>);
34
+ type: string;
35
+ options: Record<string, SpecOptions<any>>;
36
+ describe(name: any): {
37
+ fallback: {};
38
+ fields: Record<string, string>[];
39
+ };
40
+ }
41
+ export class _LiteralSpec {
42
+ /**
43
+ * @param {string} type
44
+ * @param {SpecOptions<any>} options
45
+ */
46
+ constructor(type: string, options: SpecOptions<any>);
47
+ type: string;
48
+ options: SpecOptions<any>;
49
+ describe(name: any): {
50
+ fallback: any;
51
+ fields: {
52
+ variable?: string;
53
+ flag?: string;
54
+ fallback: any;
55
+ name: any;
56
+ type: string;
57
+ }[];
58
+ };
59
+ }
60
+ /**
61
+ * @typedef {object} ConfigurationOptions
62
+ * @property {(url: URL) => Promise<string | null>} readTextFile
63
+ * @property {(key: string) => (string | undefined)} getEnvironmentVariable
64
+ * @property {(key: string) => (string | undefined)} getCommandArgument
65
+ * @property {(value: any) => (string | Promise<string>)} stringify
66
+ * @property {(value: string) => (any)} parse
67
+ */
68
+ export class Configuration {
69
+ static spec: symbol;
70
+ /** @param {ConfigurationOptions} options */
71
+ constructor(options: ConfigurationOptions);
72
+ /** @type {ConfigurationOptions} */ options: ConfigurationOptions;
73
+ /**
74
+ * @template {Record<string, Structure<any>>} T
75
+ * @param {T} options
76
+ * @returns {Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]> }>}
77
+ */
78
+ object<T extends Record<string, Structure<any>>>(options: T): Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]>; }>;
79
+ /**
80
+ * @param {SpecOptions<string>} options
81
+ * @returns {Structure<string>}
82
+ */
83
+ string(options?: SpecOptions<string>): Structure<string>;
84
+ /**
85
+ * @param {SpecOptions<number>} options
86
+ * @returns {Structure<number>}
87
+ */
88
+ number(options: SpecOptions<number>): Structure<number>;
89
+ /**
90
+ * @param {SpecOptions<boolean>} options
91
+ * @returns {Structure<number>}
92
+ */
93
+ boolean(options: SpecOptions<boolean>): Structure<number>;
94
+ /**
95
+ * @param {SpecOptions<string|URL>} options
96
+ * @returns {Structure<URL>}
97
+ */
98
+ url(options: SpecOptions<string | URL>): Structure<URL>;
99
+ /**
100
+ * @template T
101
+ * @param {SpecOptions<T>} options
102
+ * @returns {ConfigurationResult<T>}
103
+ */
104
+ _getValue<T_1>(options: SpecOptions<T_1>): ConfigurationResult<T_1>;
105
+ /** @param {ConfigurationResult<number>} result */
106
+ _parseFloat(result: ConfigurationResult<number>): number;
107
+ /** @param {ConfigurationResult<boolean>} result */
108
+ _parseBoolean(result: ConfigurationResult<boolean>): any;
109
+ /**
110
+ * @template T
111
+ * @param {URL} url
112
+ * @param {Structure<T>} spec
113
+ * @returns {Promise<T>}
114
+ */
115
+ load<T_2>(url: URL, spec: Structure<T_2>): Promise<T_2>;
116
+ /** @param {unknown} value */
117
+ getUsage(value: unknown): string;
118
+ /**
119
+ * @param {unknown} struct
120
+ * @param {string} [prefix]
121
+ * @returns {{ config: any, fields: [string, string] }}
122
+ */
123
+ describe(value: any, prefix?: string): {
124
+ config: any;
125
+ fields: [string, string];
126
+ };
127
+ /** * @param {Structure<any>} struct */
128
+ getJSONSchema(struct: Structure<any>): {
129
+ $schema: string;
130
+ };
131
+ }
132
+ export type SpecOptions<T = any> = {
133
+ variable?: string;
134
+ flag?: string;
135
+ fallback: T;
136
+ };
137
+ export type ConfigurationResult<T> = {
138
+ source: 'argument' | 'variable' | 'fallback';
139
+ value: string | T;
140
+ };
141
+ export type ConfigurationDescription = {
142
+ fallback: unknown;
143
+ fields: Record<string, string>[];
144
+ };
145
+ export type Specification = {
146
+ type: string;
147
+ options: any;
148
+ describe: (name: string) => ConfigurationDescription;
149
+ };
150
+ export type ConfigurationOptions = {
151
+ readTextFile: (url: URL) => Promise<string | null>;
152
+ getEnvironmentVariable: (key: string) => (string | undefined);
153
+ getCommandArgument: (key: string) => (string | undefined);
154
+ stringify: (value: any) => (string | Promise<string>);
155
+ parse: (value: string) => (any);
156
+ };
157
+ import { Structure } from "./structures.js";