Node.js
This module provides the integration between Gruber and Node.js, along with platform-specific utilitites and Gruber primatives.
There are also opt-in modules for specific integrations and a polyfill if you are using an older Node.js. You have to import these with specific paths, they are not included in the default export.
Install
Gruber is available through NPM for Node.js.
npm install gruber
Integrations
There are platform-specific integrations with the Configuration, Postgres & Terminator modules:
import postgres from "postgres";
import { getConfiguration, getPostgresMigrator, getTerminator } from "gruber";
// Get a Node.js specific Configuration instance that
// loads files using 'fs' and parses them through JSON
const config = getConfiguration();
// Get a Migrator using the Node.js filesystem and
// the postgres.js library
const migrator = getPostgresMigrator({
sql: postgres("postgres://…"),
directory: new URL("./migrations/", import.meta.url),
});
// Get a terminator that listens to Node.js' process signals
const arnie = getTerminator();
Utilities
There are some Node.js specific utilities too, to help with Gruber integration and web-standards.
import { serveHTTP } from "gruber";
// Create a node:http server, wrapped with the Fetch API Request/Response objects.
serveHTTP({ port: 3000 }, async (request) => {
return Response.json({ message: "ok" });
});
Polyfil
// Import this as soon as possible to ensure
// the web-standards primatives Gruber expects are available.
import "gruber/polyfill.js";
Express
There is a middleware for using a FetchRouter with an Express application.
import express from "express"
import { FetchRouter } from "gruber";
import { expressMiddleware } from "gruber/express-router.js";
const router = new FetchRouter(…)
const app = express()
.use(…)
.use(expressMiddleware(router))
.use(…)
Koa
There is a middleware for using a FetchRouter with a koa application.
import Koa from "koa"
import { FetchRouter } from "gruber";
import { koaMiddleware } from "gruber/koa-router.js";
const router = new FetchRouter(…)
const app = new Koa()
.use(…)
.use(koaMiddleware(router));
.use(…)
Miscellaneous
getConfigurationOptions
Generate standardish options to create a Configuration from the Node.js environment that reads JSON files.
- It uses parseArgs from
node:utilto parse CLI arguments - It uses promises.readFile from
node:fsto read text files - It reads environment variables from
node:process - It parses and stringifies configuration using
JSON
const options = getConfigurationOptions()
getConfiguration
Create a standardish Node.js Configuration. It creates a new Configuration object using getConfigurationOptions.
const config = getConfiguration()
createStoppable
A port of stoppable.js, ported to Gruber to reduce external dependencies and simplify.
import http from 'node:http'
const server = http.createServer(…)
const stop = createStoppable(server, { grace: 10_000 })
// …
const wasGraceful = await stop()
It keeps a record of all sockets connecting to the server so they can be gracefully closed
when the server is stopped. If they aren't closed after the grace period,
they're forcefully closed instead.
NodeRouter
A HTTP router for pure Node.js, you should probably use serveHTTP
import http from "node:http";
const router = new NodeRouter(…)
const server = http.createServer(router.forHttpServer())
server.listen(3000)
getPostgresMigratorOptions
Create a standardish Postgres Migrator based on the filesystem and an sql connection from postgres.js
const sql = postgres(…)
const migrator = getPostgresMigratorOptions({
sql,
directory: new URL("./migrations/", import.meta.url)
})
getPostgresMigrator
This is a syntax sugar for new Migrator(getPostgresMigratorOptions(...))
Container
unstable
Container holds a set of dependencies that are lazily computed and provides a system to override those dependencies during testing
const container = new Container({
message: () => 'hello there',
store: useStore
})
// Retrieve a dependency
console.log(container.get('message')) // outputs "hello there"
// Override dependencies
container.override({
store: new MemoryStore()
})
// get the overridden store
let store = container.get('store') // MemoryStore
// attempt to get the message
container.get('message') // throws Error('unmet dependency')
// restore the container back to the original dependencies
container.reset()
get
Get a dependency. First checking overrides, then previously computed or finaly use the dependency factory
override
Override the dependencies within the container or create unmet dependencies for those not-provided
// Replace the store with an in-memory one
container.override({ store: new MemoryStore() })
proxy
Create a proxy around an object that injects our dependencies
const container = new Container({ message: () => 'hello there' })
const proxy = container.proxy({ count: 7 })
proxy.message // 'hello there'
proxy.count // 7
// or with object destructuring
const { message, count } = container.proxy({ count: 7 })
reset
Clear any overrides on the dependencies
container.reset()
unwrap
internal
Compute a dependency from it's factory
const message = container.unwrap('message')
RandomService
type
RandomService provices an abstraction around generating random values
const random // RandomService
// Pick a number between 4 & 7 inclusively
let number = random.number(4, 7)
// Generate a UUID
let uuid = random.uuid()
// Pick an element from an array
let element = random.element([1, 2, 3, 4, 5])
useRandom
A standard implementation of RandomService using Math.random + crypto.randomUUID()
const random = useRandom()
let number = random.number(4, 7)
let uuid = random.uuid()
let element = random.element([1, 2, 3, 4, 5])
formatMarkdownTable
Given a set of records with known columns, format them into a pretty markdown table using the order from columns.
If a record does not have a specified value (it is null or undefined) it will be replaced with the fallback value.
const table = formatMarkdownTable(
[
{ name: 'Geoff Testington', age: 42 },
{ name: "Jess Smith", age: 32 },
{ name: "Tyler Rockwell" },
],
['name', 'age'],
'~'
)
Which will generate:
| name | age |
| ---------------- | --- |
| Geoff Testington | 42 |
| Jess Smith | 32 |
| Tyler Rockwell | ~ |
loader
unstable
loader let's you memoize the result of a function to create a singleton from it.
It works synchronously or with promises.
let index = 1
const useMessage = loader(() = 'hello there ${i++}')
useMessage() // hello there 1
useMessage() // hello there 1
useMessage() // hello there 1
trimIndentation
internal
trimIndentation takes a template literal (with values) and takes out the common whitespace.
Very heavily based on dedent
import { trimIndentation } from "gruber";
console.log(
trimIndentation`
Hello there!
My name is Geoff
`,
);
Which will output this, without any extra whitespace:
Hello there!
My name is Geoff
reconstructTemplateString
internal
Turn arguments from a string template literal back into a string
// 'I have 2 dogs'
reconstructTemplateString(['I have ', ' dogs'], 2)
or via template tags
// 'I have 2 dogs'
reconstructTemplateString`I have ${2} dogs`
preventExtraction
unstable
Take steps to prevent an object from being extracted from the app, inspired by crypto.subtle.importKey's extractable parameter.
This will:
- throw an error if the value are passed to JSON.stringify
- it recursively applies to nested objects, arrays and items within arrays
- seal and freeze the value and all nested objects & arrays
const config = preventExtraction({
name: "Geoff Testington",
pets: [
{ name: "Hugo" },
{ name: "Helga" },
],
favourite: {
mountain: "Cheviot"
}
})
// Any attempt to JSON-ify will result in an error
console.log(JSON.stringify(config)) // throws a TypeError
console.log(JSON.stringify(config.pets)) // throws a TypeError
console.log(JSON.stringify(config.pets[0])) // throws a TypeError
console.log(JSON.stringify(config.pets[1])) // throws a TypeError
console.log(JSON.stringify(config.favourite)) // throws a TypeError
The value will also be frozen and sealed, so any properties cannot be added, removed or modified.
dangerouslyExpose
unstable
DANGER undo a preventExtraction to allow values to be exposed.
This removes all of the precations that preventExtraction add.
console.log(
JSON.stringify(
dangerouslyExpose(appConfig.meta)
)
)
pickProperties
Create a subset of an object by picking off specific keys
const object = {
name: "Geoff Testington",
age: 42,
pets: ["Hugo", "Florence"]
}
pickProperties(object, ["name", "age"])
getOrInsert
Polyfil for Map#getOrInsert
let preferences = new Map()
let darkMode = getOrInsert(preferences, "use_dark_mode", true)
let groups = new Map()
for (let value of array) {
getOrInsert(groups, value.theme, []).push(value)
}
PromiseList
internal
A dynamic list of promises that are automatically removed when they resolve
const list = new PromiseList()
// Add a promise that waits for 5 seconds
list.push(async () => {
await new Promise(r => setTimeout(r, 5_000))
// Add dependant promises too
list.push(async () => {
await somethingElse()
})
})
// Wait for all promises and dependants to resolve in one go
await promises.all()
all
Wait for all promises to be resolved using Promise.all.
If new promises are added as a result of waiting, they are also awaited.
await list.all()
length
Get the current number of promises in the list
list.length // 5
push
Add a promise to the list using a factory method,
the factory just needs to return a promise
list.push(async () => {
// ...
})
includesScope
Check whether a provided scope meets the requirement of the expected scope
The idea is that a parent scope contains all children scopes, recursively. So if you find all the parents of a given scope, you can test it against a scope that has been provided by a user.
For example user:books:read will match against:
user:books:readuser:booksuser
So if any of those scopes are authorized, access can be granted.
includesScope("user:books:read", "user:books:read"); // true
includesScope("user:books", "user:books:read"); // true
includesScope("user", "user:books:read"); // true
includesScope("user", "user:podcasts"); // true
includesScope("user:books", "user:podcasts"); // false
Cors
unstable
A development utility for apply CORS headers to a HTTP server using standard Request and Response objects.
This really should not be used in production, I built this with the intention that whatever reverse-proxy the app is deployed behind would manage these headers instead.
This implementation was adapted from expressjs/cors, mostly to modernise it and remove features that weren't needed for this development-intended class.
It will:
- set
Access-Control-Allow-Methodsto all methods - mirror headers in
Access-Control-Request-Headersfrom the request - properly set the
Varyheader for any request header that varies the response - set
Access-Control-Allow-Originbased on theoptions.originsoption, allowing the origin if it is in the array or if the array includes* - set
Access-Control-Allow-Credentialsif opted in throughoptions.credentials
const cors = new Cors({
origins: ['http://localhost:8080'],
credentials: true
})
const request = new Request('http://localhost:3000/books/')
const response = Response.json({})
const result = cors.apply(request, response)
ServerSentEventMessage
type
Represents a message in the Server-Sent Event protocol
// All fields are optional
const message = {
comment: 'hello there',
event: 'my-event',
data: JSON.stringify({ lots: 'of', things: true }),
id: 42,
retry: 3600
}
comment
Ignored by the client, can be used to prevent connections from timing out
data
The data field for the message. Split by new lines.
event
A string identifying the type of event described. If specified this event is triggered, otherwise a "message" will be dispatched.
id
The event ID to set the EventSource object's last event ID value.
retry
The reconnection time. If the connection to the server is lost, the browser will wait for the specified time before attempting to reconnect.
ServerSentEventStream
Transforms server-sent message objects into strings for the client. more info.
You can then write ServerSentEventMessage to that stream over time.
const data = [{ data: "hello there" }]
// Get a stream somehow, then pipe it through
const stream = ReadableStream.from<ServerSentEventMessage>(data)
.pipeThrough(new ServerSentEventStream());
const response = new Response(stream, {
headers: {
"content-type": "text/event-stream",
"cache-control": "no-cache",
},
});
getPostgresMigrations
Query the postgres database to find migrations that have already been performed.
Returning an array of PostgresMigrationRecord.
const sql // SqlDependency
const records = await getPostgresMigrations(sql)
executePostgresMigration
Perform either the up or down postgres migration and record what happened. This will first start a transaction, so if anything goes wrong the whole operation is aborted. Within the transaction, it attempts the run the action (either up or down) as specified.
After the action is ran, it will follow up with updating the migration records.
For an up action, it will create a new PostgresMigrationRecord
and insert it into the database.
For a down action, it will remove the corresponding PostgresMigrationRecord.
There is an edge case where it will not remove the record if running the
postgresBootstrapMigrationaction, because that migration deletes the migration table itself so would be pointless.
const sql // SqlDependency
const definition = definePostgresMigration(...)
await executePostgresMigration(definition, "up", sql)
PostgresMigrationRecord
type
A record in a postgres database containing information about a migration that has been run.
const record = {
name: '001-add-users-table.js',
created: new Date()
}
postgresBootstrapMigration
This is a MigrationDefinition to bootstrap postgres migrations.
It sets up the initial "migrations" table that all other
migrations will be recorded in.
definePostgresMigration
A typed version of defineMigration that specalizes for a PostgresService.
This is mostly useful to get a strongly typed sql parameter.
import { definePostgresMigration } from "gruber"
export default definePostgresMigration({
async up(sql) {
await sql.execute`
CREATE TABLE users ...
`
},
async down(sql) {
await sql.execute`
DROP TABLE users
`
}
})
PostgresClient
type
Something that manages a connection to a postgres database and performs queries & transactions
const pg = {
execute(strings, ...values) {},
transaction() {},
dispose() {},
[Symbol.asyncDispose]() {}
}
HTTP
applyResponse
Send a web-standards Response to a Node.js ServerResponse
import http from "node:http"
http.createServer((req, res) => {
applyResponse(
Response.json({ msg: "ok" }),
res
)
})
getFetchRequest
Convert a Node.js IncomingMessage into a web-standards Request
import http from "node:http"
http.createServer((req, res) => {
let request = getFetchRequest(req)
// ...
})
getFetchHeaders
Parse Node.js IncomingHttpHeaders into a web-standards Headers object
const headers = getFetchHeaders({ accept: "text/plain" }) // Headers
getIncomingMessageBody
Convert the body of a Node.js IncomingMessage into a Streams API ReadableStream
import http from "node:http"
http.createServer((req, res) => {
let stream = getIncomingMessageBody(req)
// ...
})
getResponseReadable
Convert a Streams API ReadableStream into a Readable to later be piped to a Node.js ServerResponse
import http from "node:http"
http.createServer((req, res) => {
const webResponse = Response.json({ msg: "OK" })
getResponseReadable(webResponse, res).pipe(res)
})
Pass the second, res parameter if you'd like to terminate the web Response if Node.js is terminated.
serveHTTP
unstable
A simple abstraction for creating a HTTP server, converting Node.js primatives into Fetch API objects and handling requests through a FetchRouter.
const server = await serveHTTP({ port: 3000 }, async (request) => {
return new Response('Hello, There!')
})
This method returns a node:http Server after waiting for it to start listening.
The server has an extra stop method and also implements [Symbol.asyncDispose].
When stop is called, or it is dispoed with the using keyword, it will attempt to gracefully shutdown the HTTP server,
attempting to terminate each connection. If you created the server with a grace option,
it will wait for that maximum time before forcing every connection to close.
To quote the author of stoppable, this is "the way you probably expected it to work by default".
async function main() {
await using server = await serveHTTP(
{ port: 3000, grace: 5000 },
() => new Response('ok')
)
// …
}
await main()
When main function exits, it will automatically close the server with a 5 second grace period.
The stop method is also useful when used with a Terminator.
Configuration
ConfigurationOptions
type
Options for creating a platform-sepcific Configuration object, different methods provide abstractions over the filesystem & parsing capabilities of the Configuration.
For instance, you could create one that loads remote files over S3 and parses them as YAML, or just a simple one that loads JSON files from the filesystem
getCommandArgument
Get a specific CLI option, like --some-thing, or undefined if it is not set
getEnvironmentVariable
Get a specific environment variable, or undefined if it is not set
parse
Parse a text file into in-memory values
readTextFile
Read in a file and decode it as text, or return null if it doesn't exist
stringify
Convert an in-memory value to a string for displaying to the user
Configuration
Configuration is both an abstraction around processing config files, environment variables & CLI flags from the platform and also a tool for users to declaratively define how their configuration is.
Each platform specifies a default options to load JSON files,
but you can also construct your own if you want to customise how it works.
With an instance, you can then define how an app's config can be specified as either configuration files, CLI flag, environment variables or a combination of any of them.
const config = new Configuration({
readTextFile(url) {},
getEnvironmentVariable(key) {},
getCommandArgument() {},
stringify(value) {},
parse(value) {},
})
array
unstable
Create an ordered list of another type
config.array(
Structure.string()
)
boolean
Define a boolean value with options to load from the config-file, an environment variable or a CLI flag. The only required field is fallback
There are extra coercions for boolean-like strings
1,true&yescoerce to true0,false&nocoerce to false
config.boolean({
variable: "USE_SSL",
flag: "--ssl",
fallback: false
})
external
unstable
Load another configuration file or use value in the original configuration
config.external(
new URL("./api-keys.json", import.meta.url),
config.object({
keys: Structure.array(Structure.string())
})
)
Which will attempt to load "api-keys.json" and parse that, and if that doesn't exist it will also try the value in the original configuration.
getJSONSchema
unstable
Given a structure defined using configuration, generate a JSON Schema to validate it. This could be useful to write to a file then use a IDE-based validator using something like
{
"$schema": "./app-config.schema.json",
}
getUsage
Given a structure defined using Configuration, generate human-readable usage information. The usage includes a table of all configuration options and what the default value would be if no other soruces are used.
Optionally, output the current value of the configuration too.
load
Load configuration with a base file, also pulling in environment variables and CLI flags using ConfigurationOptions
const struct = config.object({
env: config.string({ variable: "NODE_ENV", fallback: "development" })
})
config.load(
new URL("./app-config.json", import.meta.url),
struct
)
It will asynchronously load the configuration, validate it and return the coerced value. If it fails it will output a friendly string listing what is wrong and throw the Structure.Error
number
Define a numeric value with options to load from the config-file, an environment variable or a CLI flag. The only required field is fallback
It will also coerce floating point numbers from strings
config.number({
variable: "PORT",
flag: "--port",
fallback: "1234"
})
object
Group or nest configuration in an object.
config.object({
name: config.string({ fallback: "Geoff Testington" }),
age: config.number({ fallback: 42 }),
})
string
Define a string-based value with options to load from the config-file, an environment variable or a CLI flag. The only required field is fallback
config.string({
variable: "HOSTNAME",
flag: "--host",
fallback: "localhost"
})
url
Define a URL based value, the value is validated and converted into a URL.
config.url({
variable: "SELF_URL",
flag: "--url",
fallback: "http://localhost:1234"
})
Structure
Structure.Error
An error produced from processing a value for a Structure
const error = new Structure.Error("Expected something", ["some", "path"])
It takes a message, path & children in the constructor.
You can also iterate over a Structure.Error to walk the tree of errors.
for (const error2 of error) {
console.log(error2.getOneLiner())
}
getOneLiner
Get a single-line variant, describing the error
error.getOneLiner()
which outputs something like:
some.path — expected a number
getStandardSchemaIssues
Convert the error to a StandardSchema issue to be used with that ecosystem.
toFriendlyString
Generate a human-friendly string describing the error and all nested errors
error.toFriendlyString()
which outputs something like:
Object does not match schema
name — expected a string
age — expected a number
_StructError.chain
internal
Create a new error with the context added to it
const nested = Structure.Error.chain(
new Error("Something went wrong"),
["some", "path"]
)
Structure
Structure is a composable primative for processing values to make sure they are what you expect them to be, optionally coercing the value into something else. It's also strongly-typed so values that are validated have the correct TypeScript type too.
The Structure class also supports StandardSchema v1 so you can use it anywhere that supports that standard.
getFullSchema
Get a JSON schema from the structure, where equivalent fields are available.
process
Execute the structure by passing it a value and getting back the result if it is successful, otherwise a Structure.Error is thrown
Structure.any
Define a Structure that lets any value through
Structure.any()
Structure.array
Define a list of values that each match the same structure.
// An array of strings
Structure.array(
Structure.string()
)
// An array of objects
Structure.array(
Structure.object({
name: Structure.string(),
age: Structure.number()
})
)
Structure.boolean
Define a boolean value with an optional fallback.
Structure.boolean()
Structure.boolean(false)
Structure.date
Creates a Structure that validates dates or values that can be turned into dates through the Date constructor.
Structure.date()
Structure.enum
Creates a Structure that validates a value is one of a set of literals
Structure.enum(['a string', 42, false])
NOTE: needs some TypeScript fiddling to get the generics right
Structure.fromJSONSchema
unstable
Attempts to create a Structure from a parsed JSON Schema value. This is implemented on a as-needed bases, currently it supports:
- "const" →
Structure.literal - type=string →
Structure.string - type=number →
Structure.number - type=boolean →
Structure.boolean - type=array → "items" are recursively parsed and put into a
Structure.array - type=object → "properties" are recursively parsed and put into a
Structure.object - anyOf →
Structure.union
Structure.fromJSONSchema({ type: "string" })
Structure.fromJSONSchema({ type: "number" })
Structure.fromJSONSchema({ type: "boolean" })
Structure.fromJSONSchema({
type: "object",
properties: {
name: { type:"string" },
age: { type: "number" }
},
required: ["name"]
})
Structure.fromJSONSchema({ type: "array", items: { type: "string" } })
Structure.fromJSONSchema({
anyOf: [
{ type: "string" },
{ type: "number" }
]
});
notes
- array "prefixItems" are not supported, maybe they could be mapped to tuples?
Structure.literal
Define a specific value that must be exactly equal.
Structure.literal("click_event")
Structure.literal(42)
Structure.literal(true)
Structure.null
Define a Structure to validate the value is null
Structure.null()
Structure.nullable
Creates a Structure that validates a value is either another structure or a null value
Structure.nullable(Structure.string())
Structure.number
Define a number-based value with an optional fallback,
it will also try to parse floating-point values from strings.
Structure.number()
Structure.number("Geoff Testington")
Structure.object
Define a group of structures under an object. Each field needs to matched their respective Structures and no additionaly fields are allowed.
Structure.object({
name: Structure.string(),
age: Structure.number(),
})
Structure.optional
Creates a Structure that validates another structure or is not defined
Structure.optional(Structure.string())
Structure.partial
Create a Structure that validates an object where some or none of the fields match their respective Structures. Only fields specified may be set, nothing additional.
Structure.partial({
name: Structure.string(),
age: Structure.number()
})
Structure.pick
unstable
Define an interface over an object that plucks off known values and ignores the rest.
Structure.type({
name: Structure.string(),
age: Structure.number(),
})
Structure.record
unstable
Creates a Structure for objects that map a key to a common type of value
Structure.record(Structure.string(), Structure.number())
Structure.record(
Structure.string(),
Structure.object({ name: Structure.string() })
)
Structure.record(
Structure.enum(["name", "address", "emailAddress"]),
Structure.string()
)
Structure.string
Define a string-based value with an optional fallback.
Structure.string()
Structure.string("Geoff Testington")
Structure.tuple
unstable
Creates a Structure for arrays where each index has a different validation
Structure.tuple([Structure.string(), Structure.number()])
Structure.undefined
Creates a Structure that validates a value is the undefined value
Structure.undefined()
Structure.union
Define a Structure that must match one of a set of Structures
Structure.union([
Structure.object({
type: Structure.literal("click"),
element: Structure.string()
}),
Structure.object({
type: Structure.literal("login"),
}),
])
Structure.url
Define a URL value with an optional fallback,
that will be coerced into a URL.
Structure.url()
Structure.url("http://example.com")
Structure.url(new URL("http://example.com"))
Migrator
defineMigration
Define a generic migration, this is a wrapper around creating a MigrationOptions
which within TypeScript means you can specify the <T> once, rather than for each action.
const migration = defineMigration({
up () {},
down () {},
})
loadMigration
Attempt to load a migration from a file using import.
It combines the name and directory to get a file path, attempts to import-it and convert the default export into a MigrationDefinition. You can also force the <T> parameter onto the definition.
It will throw errors if the file does not exist or if the default export doesn't look like a MigrationOptions.
const migration = await loadMigration(
'001-create-users.js',
new URL('./migrations/', import.meta.url)
)
migration.name // "001-create-users.js"
migration.up // function
migration.down // function
MigratorOptions
type
MigratorOptions lets your create your own migrator that performs migrations in different ways. For instance you could create one that loads a JSON "migrations" file from the filesystem.
execute
Perform or reverse a migration and update any required state
function execute(definition, direction) {
console.log('running', definition.name, direction)
if (direction === 'up') definition.up()
if (direction === 'down') definition.down()
}
getDefinitions
Get or generate the all migration definitions
function getDefinitions () {
return { name: 001-something.js', up() {}, down() {} }
}
getRecords
Query which migrations have already been performed
function getRecords () {
return [{ name: '001-something.js' }]
}
Migrator
Migrator provides methods for running a specific type of migrations. The idea is that different platforms/integrations can create a migrator that works with a specific feature they want to add migrations around, e.g. a Postgres database.
const migrator = new Migrator({
async getRecords() {},
async getDefinitions() {},
async execute(definition, direction) {}
})
down
Run any "down" migrations for migrations that have already been performed
It would be cool to specify a number here so you could run just 1 but I haven't needed this so it hasn't been properly designed yet
await migrator.up()
up
Run any pending "up" migrations
It would be cool to specify a number here so you could run just 1 but I haven't needed this so it hasn't been properly designed yet
await migrator.up()
Store
Store
type
Store is an async abstraction around a key-value engine like Redis or a JavaScript Map with extra features for storing things for set-durations
Store implements Disposable so you can use Explicit Resource Management
async function main() {
await using store = new MemoryStore(…)
await store.set('users/geoff', …)
}
delete
Remove a value from the store
await store.remove("users/geoff")
dispose
Close the store
await store.dispose()
get
Retrieve the value from the store
const value = await store.get("users/geoff")
set
Put a value into the store
await store.set(
'users/geoff',
{ name: "Geoff Testington"},
)
// Store jess for 5 minutes
await store.set(
"users/jess",
{ name: "Jess Smith" },
{ maxAge: 5 * 60 * 1_000 }
)
MemoryStore
MemoryStore is a in-memory implementation of Store that puts values into a Map and uses timers to expire data. It was mainly made for automated testing.
const store = new MemoryStore()
Terminator
TerminatorOptions
internal
type
Options for creating a Terminator instance
const options = {
timeout: 5_000,
signals: ['SIGINT', 'SIGTERM'],
startListeners(signals, handler) {},
exitProcess(statusCode, error) {},
}
exitProcess
Exit the process with a given code and optionaly log an error
signals
Which OS signals to listen for
startListeners
Register each signal with the OS and call the handler
timeout
How long to wait in the terminating state so loadbalancers can process it (milliseconds)
Terminator
internal
Terminators let you add graceful shutdown to your applications, create one with TerminatorOptions
const arnie = new Terminator({
timeout: 5_000,
signals: ['SIGINT', 'SIGTERM'],
startListeners(signals, handler) {},
exitProcess(statusCode, error) {},
})
getResponse
Get a Fetch Response with the state of the terminator, probably for a load balancer.
If the terminator is running, it will return a http/200 otherwise it will return a http/503
const response = await arnie.getResponse()
start
Start the terminator and capture a block of code to close the server
arnie.start(async () => {
await store.dispose()
})
terminate
internal
Start the shutdown process
await arnie.terminate(async () => {
await store.dispose()
})
waitForSignals
unstable
Experimental, wait for a terminator with promises
using store = useStore()
using server = serveHTTP(…)
await arnie.waitForSignals()
// Automatic disposal!
Tokens
TokenService
unstable
type
A service for signing and verifying access tokens
let service // TokenService
// { userId: 42, scope: "user" }
const decoded = await service.verify("some-secret-token")
// "some-secret-token"
const token = await service.sign("user", { userId: 42 })
CompositeTokens
unstable
A TokenService with multiple verification methods and a single signer
Routing
defineRoute
defineRoute is the way of specifying how your server handles a specific bit of web traffic.
It returns the RouteDefinition which can be passed around and used in various places.
Mainly it is passed to a FetchRouter to serve web requests.
export const helloRoute = defineRoute({
method: "GET",
pathname: "/hello/:name",
handler({ request, url, params }) {
return new Response(`Hello, ${params.name}!`);
}
})
FetchRouter
FetchRouter is a web-native router for routes defined with defineRoute.
const routes = [defineRoute("..."), defineRoute("..."), defineRoute("...")];
const router = new FetchRouter({ routes });
All options to the FetchRouter constructor are optional
and you can create a router without any options if you want.
routes are the route definitions you want the router to processes,
the router will handle a request based on the first route that matches.
So order is important.
errorHandler(error, request) is called if a 5xx HTTPError is caught, including unknown errors.
It is called with the offending error and the request it is associated with.
NOTE: The
errorHandlercould do more in the future, like create it's own Response or mutate the existing response. This has not been designed and is left open to future development if it becomes important.
log is an unstable option to turn on HTTP logging, it can be a boolean or middleware function. It also logs HTTP errors if not already configured through errorHandler.
cors is an unstable option to apply a CORS instance to all requests and adds an OPTIONS route handler
findMatches
Find each matching route in turn
let request = new Request('...')
for (const match of router.findMatches(request)) {
// do something with the request and/or break the loop
}
getResponse
Process all routes and get a HTTP Response.
const response = router.getResponse(
new Request('http://localhost/pathname')
)
NOTE: it would be nice to align this with the Fetch API
fetchmethod signature.
handleError
Attempt to handle an error thrown from a route's handler,
checking for well-known HTTPError instance or converting unknown errors into one.
The HTTPError is then used to convert the error into a HTTP Response.
If the error is server-based it will trigger the FetchRouter's errorHandler.
const response = router.handleError(request, new Error("Something went wrong"))
processMatches
internal
Take an iterator of route matches and convert them into a HTTP Response
by executing the route's handler.
It will return the first route to return a Response object
or throw a HTTPError if no routes matched.
const response = await router.processMatches(request, matches)
processRoute
Execute a route's handler to generate a HTTP Response
HTTPError
HTTPError
A custom Error subclass that represents an HTTP error to be returned to the user.
This allows routes to throw specific HTTP errors directly and FetchRouter knows how to handle them and turn them into HTTP Responses
You can use well-known errors like below, you can also pass a BodyInit to customise the response body.
throw HTTPError.badRequest()
throw HTTPError.unauthorized()
throw HTTPError.notFound()
throw HTTPError.internalServerError()
throw HTTPError.notImplemented()
// The plan is to add more error well-known codes as they are needed
You can also manually construct the error:
const teapot = new HTTPError(418, "I'm a teapot");
body
A custom body to send to the client
headers
Extra headers to send to the client
status
The HTTP status to return ~ status
statusText
The status text to return ~ statusText
toResponse
Convert the HTTPError into a HTTP Response object
taking into account the status, statusText and headers fields on the error.
const error = new HTTPError(418, "I'm a teapot");
error.toResponse() // Response
Validation
getRequestBody
unstable
Get and parse well-known request bodies based on the Content-Type header supplied
// Parse a application/x-www-form-urlencoded
// or multipart/form-data request
const formData = await getRequestBody(
new Request('http://localhost:8000', { body: new FormData() })
)
// Parse an application/json request
const json = await getRequestBody(
new Request('http://localhost:8000', {
body: JSON.stringify({ hello: 'world' }),
headers: { 'Content-Type': 'application/json' },
})
)
assertRequestBody
unstable
Validate the body of a request against a StandardSchema or Structure.
This will throw nice HTTPError errors that are caught by gruber and sent along to the user.
const struct = Structure.object({ name: Structure.string() })
const body1 = await assertRequestBody(struct, new Request('…'))
NOTE — you need to await the function when passing a
Request
or from a JavaScript value:
const body2 = assertRequestBody(struct, { … })
const body3 = assertRequestBody(struct, new FormData(…))
const body3 = assertRequestBody(struct, new URLSearchParams(…))
you can use any StandardSchema library with this:
import { z } from 'zod'
const body4 = assertRequestBody(
z.object({ name: z.string() }),
{ name: "Geoff Testington" }
)
debug
{
"getConfigurationOptions": {
"entrypoint": "node/mod.ts",
"id": "getConfigurationOptions",
"name": "getConfigurationOptions",
"content": "Generate standardish options to create a Configuration from the Node.js environment that reads JSON files.\n\n- It uses parseArgs from `node:util` to parse CLI arguments\n- It uses promises.readFile from `node:fs` to read text files\n- It reads environment variables from `node:process`\n- It parses and stringifies configuration using `JSON`\n\n```js\nconst options = getConfigurationOptions()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getConfiguration": {
"entrypoint": "node/mod.ts",
"id": "getConfiguration",
"name": "getConfiguration",
"content": "Create a standardish Node.js Configuration.\nIt creates a new Configuration object using [getConfigurationOptions](#getconfigurationoptions).\n\n```js\nconst config = getConfiguration()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"applyResponse": {
"entrypoint": "node/mod.ts",
"id": "applyResponse",
"name": "applyResponse",
"content": "Send a web-standards Response to a Node.js [ServerResponse](https://nodejs.org/api/http.html#class-httpserverresponse)\n\n```js\nimport http from \"node:http\"\n\nhttp.createServer((req, res) => {\n\tapplyResponse(\n\t\tResponse.json({ msg: \"ok\" }),\n\t\tres\n\t)\n})\n```",
"tags": {
"group": "HTTP"
},
"children": {}
},
"getFetchRequest": {
"entrypoint": "node/mod.ts",
"id": "getFetchRequest",
"name": "getFetchRequest",
"content": "Convert a Node.js [IncomingMessage](https://nodejs.org/api/http.html#class-httpincomingmessage) into a\nweb-standards [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)\n\n```js\nimport http from \"node:http\"\n\nhttp.createServer((req, res) => {\n\tlet request = getFetchRequest(req)\n\t// ...\n})\n```",
"tags": {
"group": "HTTP"
},
"children": {}
},
"getFetchHeaders": {
"entrypoint": "node/mod.ts",
"id": "getFetchHeaders",
"name": "getFetchHeaders",
"content": "Parse Node.js IncomingHttpHeaders into a web-standards [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object\n\n```js\nconst headers = getFetchHeaders({ accept: \"text/plain\" }) // Headers\n```",
"tags": {
"group": "HTTP"
},
"children": {}
},
"getIncomingMessageBody": {
"entrypoint": "node/mod.ts",
"id": "getIncomingMessageBody",
"name": "getIncomingMessageBody",
"content": "Convert the body of a Node.js [IncomingMessage](https://nodejs.org/api/http.html#class-httpincomingmessage) into a Streams API ReadableStream\n\n```js\nimport http from \"node:http\"\n\nhttp.createServer((req, res) => {\n\tlet stream = getIncomingMessageBody(req)\n\t// ...\n})\n```",
"tags": {
"group": "HTTP"
},
"children": {}
},
"getResponseReadable": {
"entrypoint": "node/mod.ts",
"id": "getResponseReadable",
"name": "getResponseReadable",
"content": "Convert a Streams API ReadableStream into a Readable to later be piped to a Node.js [ServerResponse](https://nodejs.org/api/http.html#class-httpserverresponse)\n\n```js\nimport http from \"node:http\"\n\nhttp.createServer((req, res) => {\n\tconst webResponse = Response.json({ msg: \"OK\" })\n\tgetResponseReadable(webResponse, res).pipe(res)\n})\n```\n\nPass the second, `res` parameter if you'd like to terminate the web Response if Node.js is terminated.",
"tags": {
"group": "HTTP"
},
"children": {}
},
"serveHTTP": {
"entrypoint": "node/mod.ts",
"id": "serveHTTP",
"name": "serveHTTP",
"content": "A simple abstraction for creating a HTTP server, converting Node.js primatives into Fetch API objects and handling requests through a FetchRouter.\n\n```js\nconst server = await serveHTTP({ port: 3000 }, async (request) => {\n\treturn new Response('Hello, There!')\n})\n```\n\nThis method returns a `node:http` Server after waiting for it to start listening.\nThe server has an extra `stop` method and also implements `[Symbol.asyncDispose]`.\n\nWhen stop is called, or it is dispoed with the `using` keyword, it will attempt to gracefully shutdown the HTTP server,\nattempting to terminate each connection. If you created the server with a `grace` option,\nit will wait for that maximum time before forcing every connection to close.\nTo quote the author of stoppable, this is \"the way you probably expected it to work by default\".\n\n```js\nasync function main() {\n\tawait using server = await serveHTTP(\n\t\t{ port: 3000, grace: 5000 },\n\t\t() => new Response('ok')\n\t)\n\t// …\n}\n\nawait main()\n```\n\nWhen main function exits, it will automatically close the server with a 5 second grace period.\n\nThe stop method is also useful when used with a [Terminator](/core/#terminator).",
"tags": {
"unstable": "true",
"group": "HTTP"
},
"children": {}
},
"createStoppable": {
"entrypoint": "node/mod.ts",
"id": "createStoppable",
"name": "createStoppable",
"content": "A port of [stoppable.js](https://github.com/hunterloftis/stoppable/blob/master/lib/stoppable.js),\nported to Gruber to reduce external dependencies and simplify.\n\n```js\nimport http from 'node:http'\n\nconst server = http.createServer(…)\nconst stop = createStoppable(server, { grace: 10_000 })\n\n// …\n\nconst wasGraceful = await stop()\n```\n\nIt keeps a record of all sockets connecting to the server so they can be gracefully closed\nwhen the server is stopped. If they aren't closed after the `grace` period,\nthey're forcefully closed instead.",
"tags": {
"hidden": "true",
"group": "Miscellaneous"
},
"children": {}
},
"NodeRouter": {
"entrypoint": "node/mod.ts",
"id": "NodeRouter",
"name": "NodeRouter",
"content": "A HTTP router for pure Node.js, you should probably use [serveHTTP](#servehttp)\n\n```js\nimport http from \"node:http\";\n\nconst router = new NodeRouter(…)\nconst server = http.createServer(router.forHttpServer())\nserver.listen(3000)\n```",
"tags": {
"hidden": "true",
"deprecated": "use {@link serveHTTP}",
"group": "Miscellaneous"
},
"children": {}
},
"getPostgresMigratorOptions": {
"entrypoint": "node/mod.ts",
"id": "getPostgresMigratorOptions",
"name": "getPostgresMigratorOptions",
"content": "Create a standardish Postgres Migrator based on the filesystem and an sql connection from [postgres.js](https://github.com/porsager/postgres)\n\n```js\nconst sql = postgres(…)\n\nconst migrator = getPostgresMigratorOptions({\n\tsql,\n\tdirectory: new URL(\"./migrations/\", import.meta.url)\n})\n\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getPostgresMigrator": {
"entrypoint": "node/mod.ts",
"id": "getPostgresMigrator",
"name": "getPostgresMigrator",
"content": "This is a syntax sugar for `new Migrator(getPostgresMigratorOptions(...))`",
"tags": {
"param": "{PostgresMigratorOptions} options",
"group": "Miscellaneous"
},
"children": {}
},
"ConfigurationOptions": {
"entrypoint": "node/mod.ts",
"id": "ConfigurationOptions",
"name": "ConfigurationOptions",
"content": "Options for creating a platform-sepcific [Configuration](#configuration) object,\ndifferent methods provide abstractions over the filesystem & parsing capabilities of the Configuration.\n\nFor instance, you could create one that loads remote files over S3 and parses them as YAML,\nor just a simple one that loads JSON files from the filesystem",
"tags": {
"group": "Configuration",
"type": "true"
},
"children": {
"readTextFile": {
"id": "ConfigurationOptions#readTextFile",
"name": "readTextFile",
"content": "Read in a file and decode it as text, or return null if it doesn't exist",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getEnvironmentVariable": {
"id": "ConfigurationOptions#getEnvironmentVariable",
"name": "getEnvironmentVariable",
"content": "Get a specific environment variable, or undefined if it is not set",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getCommandArgument": {
"id": "ConfigurationOptions#getCommandArgument",
"name": "getCommandArgument",
"content": "Get a specific CLI option, like `--some-thing`, or undefined if it is not set",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"stringify": {
"id": "ConfigurationOptions#stringify",
"name": "stringify",
"content": "Convert an in-memory value to a string for displaying to the user",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"parse": {
"id": "ConfigurationOptions#parse",
"name": "parse",
"content": "Parse a text file into in-memory values",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"Configuration": {
"entrypoint": "node/mod.ts",
"id": "Configuration",
"name": "Configuration",
"content": "**Configuration** is both an abstraction around processing config files,\nenvironment variables & CLI flags from the platform\nand also a tool for users to declaratively define how their configuration is.\n\nEach platform specifies a default `options` to load JSON files,\nbut you can also construct your own if you want to customise how it works.\n\nWith an instance, you can then define how an app's config can be specified as either configuration files,\nCLI flag, environment variables or a combination of any of them.\n\n```js\nconst config = new Configuration({\n\treadTextFile(url) {},\n\tgetEnvironmentVariable(key) {},\n\tgetCommandArgument() {},\n\tstringify(value) {},\n\tparse(value) {},\n})\n```",
"tags": {
"group": "Configuration"
},
"children": {
"object": {
"id": "Configuration#object",
"name": "object",
"content": "Group or nest configuration in an object.\n\n```js\nconfig.object({\n\tname: config.string({ fallback: \"Geoff Testington\" }),\n\tage: config.number({ fallback: 42 }),\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"array": {
"id": "Configuration#array",
"name": "array",
"content": "Create an ordered list of another type\n\n```js\nconfig.array(\n\tStructure.string()\n)\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"external": {
"id": "Configuration#external",
"name": "external",
"content": "Load another configuration file or use value in the original configuration\n\n```js\nconfig.external(\n\tnew URL(\"./api-keys.json\", import.meta.url),\n\tconfig.object({\n\t\tkeys: Structure.array(Structure.string())\n\t})\n)\n```\n\nWhich will attempt to load \"api-keys.json\" and parse that,\nand if that doesn't exist it will also try the value in the original configuration.",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"string": {
"id": "Configuration#string",
"name": "string",
"content": "Define a string-based value with options to load from the config-file,\nan environment variable or a CLI flag.\nThe only required field is **fallback**\n\n```js\nconfig.string({\n\tvariable: \"HOSTNAME\",\n\tflag: \"--host\",\n\tfallback: \"localhost\"\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"number": {
"id": "Configuration#number",
"name": "number",
"content": "Define a numeric value with options to load from the config-file,\nan environment variable or a CLI flag.\nThe only required field is **fallback**\n\nIt will also coerce floating point numbers from strings\n\n```js\nconfig.number({\n\tvariable: \"PORT\",\n\tflag: \"--port\",\n\tfallback: \"1234\"\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"boolean": {
"id": "Configuration#boolean",
"name": "boolean",
"content": "Define a boolean value with options to load from the config-file,\nan environment variable or a CLI flag.\nThe only required field is **fallback**\n\nThere are extra coercions for boolean-like strings\n\n- `1`, `true` & `yes` coerce to true\n- `0`, `false` & `no` coerce to false\n\n```js\nconfig.boolean({\n\tvariable: \"USE_SSL\",\n\tflag: \"--ssl\",\n\tfallback: false\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"url": {
"id": "Configuration#url",
"name": "url",
"content": "Define a URL based value, the value is validated and converted into a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL).\n\n```js\nconfig.url({\n\tvariable: \"SELF_URL\",\n\tflag: \"--url\",\n\tfallback: \"http://localhost:1234\"\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"load": {
"id": "Configuration#load",
"name": "load",
"content": "Load configuration with a base file, also pulling in environment variables and CLI flags using [ConfigurationOptions](#configurationoptions)\n\n```js\nconst struct = config.object({\n\tenv: config.string({ variable: \"NODE_ENV\", fallback: \"development\" })\n})\n\nconfig.load(\n\tnew URL(\"./app-config.json\", import.meta.url),\n\tstruct\n)\n```\n\nIt will asynchronously load the configuration, validate it and return the coerced value.\nIf it fails it will output a friendly string listing what is wrong and throw the Structure.Error",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getUsage": {
"id": "Configuration#getUsage",
"name": "getUsage",
"content": "Given a structure defined using Configuration, generate human-readable usage information.\nThe usage includes a table of all configuration options and what the default value would be if no other soruces are used.\n\nOptionally, output the current value of the configuration too.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getJSONSchema": {
"id": "Configuration#getJSONSchema",
"name": "getJSONSchema",
"content": "Given a structure defined using configuration, generate a JSON Schema to validate it. This could be useful to write to a file then use a IDE-based validator using something like\n\n```json\n{\n\t\"$schema\": \"./app-config.schema.json\",\n}\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
}
}
},
"Structure.Error": {
"entrypoint": "node/mod.ts",
"id": "_StructError",
"name": "Structure.Error",
"content": "An error produced from processing a value for a [Structure](#structure)\n\n```js\nconst error = new Structure.Error(\"Expected something\", [\"some\", \"path\"])\n```\n\nIt takes a `message`, `path` & `children` in the constructor.\n\nYou can also iterate over a Structure.Error to walk the tree of errors.\n\n```js\nfor (const error2 of error) {\n console.log(error2.getOneLiner())\n}\n```",
"tags": {
"name": "Structure.Error",
"group": "Structure"
},
"children": {
"getOneLiner": {
"id": "_StructError#getOneLiner",
"name": "getOneLiner",
"content": "Get a single-line variant, describing the error\n\n```js\nerror.getOneLiner()\n```\n\nwhich outputs something like:\n\n```\nsome.path — expected a number\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"toFriendlyString": {
"id": "_StructError#toFriendlyString",
"name": "toFriendlyString",
"content": "Generate a human-friendly string describing the error and all nested errors\n\n```js\nerror.toFriendlyString()\n```\n\nwhich outputs something like:\n\n```\nObject does not match schema\n name — expected a string\n age — expected a number\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getStandardSchemaIssues": {
"id": "_StructError#getStandardSchemaIssues",
"name": "getStandardSchemaIssues",
"content": "Convert the error to a [StandardSchema](https://standardschema.dev/) issue to be used with that ecosystem.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"_StructError.chain": {
"id": "_StructError.chain",
"name": "chain",
"content": "Create a new error with the context added to it\n\n```js\nconst nested = Structure.Error.chain(\n\tnew Error(\"Something went wrong\"),\n\t[\"some\", \"path\"]\n)\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
}
}
},
"Structure": {
"entrypoint": "node/mod.ts",
"id": "Structure",
"name": "Structure",
"content": "**Structure** is a composable primative for processing values to make sure they are what you expect them to be, optionally coercing the value into something else. It's also strongly-typed so values that are validated have the correct TypeScript type too.\n\nThe Structure class also supports [StandardSchema v1](https://standardschema.dev) so you can use it anywhere that supports that standard.",
"tags": {
"group": "Structure"
},
"children": {
"process": {
"id": "Structure#process",
"name": "process",
"content": "Execute the structure by passing it a value and getting back the result if it is successful, otherwise a [Structure.Error](#structure-error) is thrown",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getFullSchema": {
"id": "Structure#getFullSchema",
"name": "getFullSchema",
"content": "Get a JSON schema from the structure, where equivalent fields are available.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.string": {
"id": "Structure.string",
"name": "string",
"content": "Define a string-based value with an optional `fallback`.\n\n```js\nStructure.string()\nStructure.string(\"Geoff Testington\")\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.number": {
"id": "Structure.number",
"name": "number",
"content": "Define a number-based value with an optional `fallback`,\nit will also try to parse floating-point values from strings.\n\n```js\nStructure.number()\nStructure.number(\"Geoff Testington\")\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.boolean": {
"id": "Structure.boolean",
"name": "boolean",
"content": "Define a boolean value with an optional `fallback`.\n\n```js\nStructure.boolean()\nStructure.boolean(false)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.url": {
"id": "Structure.url",
"name": "url",
"content": "Define a URL value with an optional `fallback`,\nthat will be coerced into a `URL`.\n\n```js\nStructure.url()\nStructure.url(\"http://example.com\")\nStructure.url(new URL(\"http://example.com\"))\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.object": {
"id": "Structure.object",
"name": "object",
"content": "Define a group of structures under an object.\nEach field needs to matched their respective Structures and no additionaly fields are allowed.\n\n```js\nStructure.object({\n\tname: Structure.string(),\n\tage: Structure.number(),\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.pick": {
"id": "Structure.pick",
"name": "pick",
"content": "Define an interface over an object that plucks off known values and ignores the rest.\n\n```js\nStructure.type({\n\tname: Structure.string(),\n\tage: Structure.number(),\n})\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"Structure.array": {
"id": "Structure.array",
"name": "array",
"content": "Define a list of values that each match the same structure.\n\n```js\n// An array of strings\nStructure.array(\n\tStructure.string()\n)\n\n// An array of objects\nStructure.array(\n\tStructure.object({\n\t\tname: Structure.string(),\n\t\tage: Structure.number()\n\t})\n)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.literal": {
"id": "Structure.literal",
"name": "literal",
"content": "Define a specific value that must be exactly equal.\n\n\n```js\nStructure.literal(\"click_event\")\nStructure.literal(42)\nStructure.literal(true)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.union": {
"id": "Structure.union",
"name": "union",
"content": "Define a Structure that must match one of a set of Structures\n\n```js\nStructure.union([\n\tStructure.object({\n\t\ttype: Structure.literal(\"click\"),\n\t\telement: Structure.string()\n\t}),\n\tStructure.object({\n\t\ttype: Structure.literal(\"login\"),\n\t}),\n])\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.fromJSONSchema": {
"id": "Structure.fromJSONSchema",
"name": "fromJSONSchema",
"content": "Attempts to create a Structure from a parsed [JSON Schema](https://json-schema.org/specification) value.\nThis is implemented on a as-needed bases, currently it supports:\n- \"const\" → `Structure.literal`\n- type=string → `Structure.string`\n- type=number → `Structure.number`\n- type=boolean → `Structure.boolean`\n- type=array → \"items\" are recursively parsed and put into a `Structure.array`\n- type=object → \"properties\" are recursively parsed and put into a `Structure.object`\n- anyOf → `Structure.union`\n\n```js\nStructure.fromJSONSchema({ type: \"string\" })\nStructure.fromJSONSchema({ type: \"number\" })\nStructure.fromJSONSchema({ type: \"boolean\" })\nStructure.fromJSONSchema({\n type: \"object\",\n properties: {\n name: { type:\"string\" },\n age: { type: \"number\" }\n },\n required: [\"name\"]\n})\nStructure.fromJSONSchema({ type: \"array\", items: { type: \"string\" } })\nStructure.fromJSONSchema({\n anyOf: [\n { type: \"string\" },\n { type: \"number\" }\n ]\n});\n```\n\nnotes\n- array \"prefixItems\" are not supported, maybe they could be mapped to tuples?",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"Structure.tuple": {
"id": "Structure.tuple",
"name": "tuple",
"content": "Creates a Structure for arrays where each index has a different validation\n\n```js\nStructure.tuple([Structure.string(), Structure.number()])\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"Structure.record": {
"id": "Structure.record",
"name": "record",
"content": "Creates a Structure for objects that map a key to a common type of value\n\n```js\nStructure.record(Structure.string(), Structure.number())\nStructure.record(\n Structure.string(),\n Structure.object({ name: Structure.string() })\n)\nStructure.record(\n Structure.enum([\"name\", \"address\", \"emailAddress\"]),\n Structure.string()\n)\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"Structure.null": {
"id": "Structure.null",
"name": "null",
"content": "Define a Structure to validate the value is `null`\n\n```js\nStructure.null()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.any": {
"id": "Structure.any",
"name": "any",
"content": "Define a Structure that lets any value through\n\n```js\nStructure.any()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.partial": {
"id": "Structure.partial",
"name": "partial",
"content": "Create a Structure that validates an object where some or none of the fields match their respective Structures.\nOnly fields specified may be set, nothing additional.\n\n```js\nStructure.partial({\n\tname: Structure.string(),\n\tage: Structure.number()\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.date": {
"id": "Structure.date",
"name": "date",
"content": "Creates a Structure that validates dates or values that can be turned into dates\nthrough the [Date constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date).\n\n```js\nStructure.date()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.nullable": {
"id": "Structure.nullable",
"name": "nullable",
"content": "Creates a Structure that validates a value is either another structure or a null value\n\n```js\nStructure.nullable(Structure.string())\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.enum": {
"id": "Structure.enum",
"name": "enum",
"content": "Creates a Structure that validates a value is one of a set of literals\n\n```js\nStructure.enum(['a string', 42, false])\n```\n\nNOTE: needs some TypeScript fiddling to get the generics right",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.undefined": {
"id": "Structure.undefined",
"name": "undefined",
"content": "Creates a Structure that validates a value is the `undefined` value\n\n```js\nStructure.undefined()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.optional": {
"id": "Structure.optional",
"name": "optional",
"content": "Creates a Structure that validates another structure or is not defined\n\n```js\nStructure.optional(Structure.string())\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"Container": {
"entrypoint": "node/mod.ts",
"id": "Container",
"name": "Container",
"content": "Container holds a set of dependencies that are lazily computed\nand provides a system to override those dependencies during testing\n\n```js\nconst container = new Container({\n message: () => 'hello there',\n store: useStore\n})\n\n// Retrieve a dependency\nconsole.log(container.get('message')) // outputs \"hello there\"\n\n// Override dependencies\ncontainer.override({\n store: new MemoryStore()\n})\n\n// get the overridden store\nlet store = container.get('store') // MemoryStore\n\n// attempt to get the message\ncontainer.get('message') // throws Error('unmet dependency')\n\n// restore the container back to the original dependencies\ncontainer.reset()\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {
"override": {
"id": "Container#override",
"name": "override",
"content": "Override the dependencies within the container or create unmet dependencies for those not-provided\n\n```js\n// Replace the store with an in-memory one\ncontainer.override({ store: new MemoryStore() })\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"reset": {
"id": "Container#reset",
"name": "reset",
"content": "Clear any overrides on the dependencies\n\n```js\ncontainer.reset()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"get": {
"id": "Container#get",
"name": "get",
"content": "Get a dependency. First checking overrides, then previously computed or finaly use the dependency factory",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"unwrap": {
"id": "Container#unwrap",
"name": "unwrap",
"content": "Compute a dependency from it's factory\n\n```js\nconst message = container.unwrap('message')\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
},
"proxy": {
"id": "Container#proxy",
"name": "proxy",
"content": "Create a proxy around an object that injects our dependencies\n\n```ts\nconst container = new Container({ message: () => 'hello there' })\n\nconst proxy = container.proxy({ count: 7 })\nproxy.message // 'hello there'\nproxy.count // 7\n\n// or with object destructuring\nconst { message, count } = container.proxy({ count: 7 })\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"defineMigration": {
"entrypoint": "node/mod.ts",
"id": "defineMigration",
"name": "defineMigration",
"content": "Define a generic migration, this is a wrapper around creating a `MigrationOptions`\nwhich within TypeScript means you can specify the `` once, rather than for each action.\n\n```js\nconst migration = defineMigration({\n up () {},\n down () {},\n})\n```",
"tags": {
"group": "Migrator"
},
"children": {}
},
"loadMigration": {
"entrypoint": "node/mod.ts",
"id": "loadMigration",
"name": "loadMigration",
"content": "Attempt to load a migration from a file using `import`.\n\nIt combines the `name` and `directory` to get a file path, attempts to `import`-it and convert the `default` export into a `MigrationDefinition`. You can also force the `` parameter onto the definition.\n\nIt will throw errors if the file does not exist or if the default export doesn't look like a `MigrationOptions`.\n\n\n```js\nconst migration = await loadMigration(\n '001-create-users.js',\n new URL('./migrations/', import.meta.url)\n)\n\nmigration.name // \"001-create-users.js\"\nmigration.up // function\nmigration.down // function\n```",
"tags": {
"group": "Migrator"
},
"children": {}
},
"MigratorOptions": {
"entrypoint": "node/mod.ts",
"id": "MigratorOptions",
"name": "MigratorOptions",
"content": "MigratorOptions lets your create your own migrator that performs migrations in different ways.\nFor instance you could create one that loads a JSON \"migrations\" file from the filesystem.",
"tags": {
"group": "Migrator",
"type": "true"
},
"children": {
"getDefinitions": {
"id": "MigratorOptions#getDefinitions",
"name": "getDefinitions",
"content": "Get or generate the all migration definitions\n\n```js\nfunction getDefinitions () {\n\treturn { name: 001-something.js', up() {}, down() {} }\n}\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getRecords": {
"id": "MigratorOptions#getRecords",
"name": "getRecords",
"content": "Query which migrations have already been performed\n\n```js\nfunction getRecords () {\n\treturn [{ name: '001-something.js' }]\n}\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"execute": {
"id": "MigratorOptions#execute",
"name": "execute",
"content": "Perform or reverse a migration and update any required state\n\n```js\nfunction execute(definition, direction) {\n\tconsole.log('running', definition.name, direction)\n\tif (direction === 'up') definition.up()\n\tif (direction === 'down') definition.down()\n}\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"Migrator": {
"entrypoint": "node/mod.ts",
"id": "Migrator",
"name": "Migrator",
"content": "Migrator provides methods for running a specific type of migrations.\nThe idea is that different platforms/integrations can create a migrator that\nworks with a specific feature they want to add migrations around, e.g. a Postgres database.\n\n```js\nconst migrator = new Migrator({\n\tasync getRecords() {},\n\tasync getDefinitions() {},\n\tasync execute(definition, direction) {}\n})\n```\n\nSee [examples/node-fs-migrator](https://github.com/robb-j/gruber/tree/main/examples/node-fs-migrator)",
"tags": {
"group": "Migrator"
},
"children": {
"up": {
"id": "Migrator#up",
"name": "up",
"content": "Run any pending \"up\" migrations\n\n> It would be cool to specify a number here so you could run just 1 but\n> I haven't needed this so it hasn't been properly designed yet\n\n```js\nawait migrator.up()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"down": {
"id": "Migrator#down",
"name": "down",
"content": "Run any \"down\" migrations for migrations that have already been performed\n\n> It would be cool to specify a number here so you could run just 1 but\n> I haven't needed this so it hasn't been properly designed yet\n\n```js\nawait migrator.up()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"RandomService": {
"entrypoint": "node/mod.ts",
"id": "RandomService",
"name": "RandomService",
"content": "RandomService provices an abstraction around generating random values\n\n```js\nconst random // RandomService\n\n// Pick a number between 4 & 7 inclusively\nlet number = random.number(4, 7)\n\n// Generate a UUID\nlet uuid = random.uuid()\n\n// Pick an element from an array\nlet element = random.element([1, 2, 3, 4, 5])\n```",
"tags": {
"group": "Miscellaneous",
"type": "true"
},
"children": {}
},
"useRandom": {
"entrypoint": "node/mod.ts",
"id": "useRandom",
"name": "useRandom",
"content": "A standard implementation of `RandomService` using Math.random + crypto.randomUUID()\n\n```js\nconst random = useRandom()\nlet number = random.number(4, 7)\nlet uuid = random.uuid()\nlet element = random.element([1, 2, 3, 4, 5])\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Store": {
"entrypoint": "node/mod.ts",
"id": "Store",
"name": "Store",
"content": "Store is an async abstraction around a key-value engine like Redis or a JavaScript Map\nwith extra features for storing things for set-durations\n\nStore implements Disposable so you can use Explicit Resource Management\n\n```js\nasync function main() {\n await using store = new MemoryStore(…)\n\n await store.set('users/geoff', …)\n}\n```",
"tags": {
"group": "Store",
"type": "true"
},
"children": {
"get": {
"id": "Store#get",
"name": "get",
"content": "Retrieve the value from the store\n\n```js\nconst value = await store.get(\"users/geoff\")\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"set": {
"id": "Store#set",
"name": "set",
"content": "Put a value into the store\n\n```js\nawait store.set(\n 'users/geoff',\n { name: \"Geoff Testington\"},\n)\n\n// Store jess for 5 minutes\nawait store.set(\n \"users/jess\",\n { name: \"Jess Smith\" },\n { maxAge: 5 * 60 * 1_000 }\n)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"delete": {
"id": "Store#delete",
"name": "delete",
"content": "Remove a value from the store\n\n```js\nawait store.remove(\"users/geoff\")\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"dispose": {
"id": "Store#dispose",
"name": "dispose",
"content": "Close the store\n\n```js\nawait store.dispose()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"MemoryStore": {
"entrypoint": "node/mod.ts",
"id": "MemoryStore",
"name": "MemoryStore",
"content": "MemoryStore is a in-memory implementation of [Store](#store) that puts values into a Map and uses timers to expire data.\nIt was mainly made for automated testing.\n\n```js\nconst store = new MemoryStore()\n```",
"tags": {
"group": "Store"
},
"children": {}
},
"TerminatorOptions": {
"entrypoint": "node/mod.ts",
"id": "TerminatorOptions",
"name": "TerminatorOptions",
"content": "Options for creating a [Terminator](#terminator) instance\n\n```js\nconst options = {\n timeout: 5_000,\n signals: ['SIGINT', 'SIGTERM'],\n startListeners(signals, handler) {},\n exitProcess(statusCode, error) {},\n}\n```",
"tags": {
"internal": "true",
"group": "Terminator",
"type": "true"
},
"children": {
"timeout": {
"id": "TerminatorOptions#timeout",
"name": "timeout",
"content": "How long to wait in the terminating state so loadbalancers can process it (milliseconds)",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"signals": {
"id": "TerminatorOptions#signals",
"name": "signals",
"content": "Which OS signals to listen for",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"startListeners": {
"id": "TerminatorOptions#startListeners",
"name": "startListeners",
"content": "Register each signal with the OS and call the handler",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"exitProcess": {
"id": "TerminatorOptions#exitProcess",
"name": "exitProcess",
"content": "Exit the process with a given code and optionaly log an error",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"Terminator": {
"entrypoint": "node/mod.ts",
"id": "Terminator",
"name": "Terminator",
"content": "Terminators let you add graceful shutdown to your applications,\ncreate one with [TerminatorOptions](#terminatoroptions)\n\n```js\nconst arnie = new Terminator({\n timeout: 5_000,\n signals: ['SIGINT', 'SIGTERM'],\n startListeners(signals, handler) {},\n exitProcess(statusCode, error) {},\n})\n```",
"tags": {
"internal": "true",
"group": "Terminator"
},
"children": {
"start": {
"id": "Terminator#start",
"name": "start",
"content": "Start the terminator and capture a block of code to close the server\n\n```js\narnie.start(async () => {\n await store.dispose()\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"terminate": {
"id": "Terminator#terminate",
"name": "terminate",
"content": "Start the shutdown process\n\n```js\nawait arnie.terminate(async () => {\n await store.dispose()\n})\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
},
"getResponse": {
"id": "Terminator#getResponse",
"name": "getResponse",
"content": "Get a Fetch Response with the state of the terminator, probably for a load balancer.\n\nIf the terminator is running, it will return a http/200\notherwise it will return a http/503\n\n```js\nconst response = await arnie.getResponse()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"waitForSignals": {
"id": "Terminator#waitForSignals",
"name": "waitForSignals",
"content": "Experimental, wait for a terminator with promises\n\n```js\nusing store = useStore()\nusing server = serveHTTP(…)\n\nawait arnie.waitForSignals()\n\n// Automatic disposal!\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
}
}
},
"TokenService": {
"entrypoint": "node/mod.ts",
"id": "TokenService",
"name": "TokenService",
"content": "A service for signing and verifying access tokens\n\n```js\nlet service // TokenService\n\n// { userId: 42, scope: \"user\" }\nconst decoded = await service.verify(\"some-secret-token\")\n\n// \"some-secret-token\"\nconst token = await service.sign(\"user\", { userId: 42 })\n```",
"tags": {
"unstable": "true",
"group": "Tokens",
"type": "true"
},
"children": {}
},
"CompositeTokens": {
"entrypoint": "node/mod.ts",
"id": "CompositeTokens",
"name": "CompositeTokens",
"content": "A TokenService with multiple verification methods and a single signer",
"tags": {
"unstable": "true",
"group": "Tokens"
},
"children": {}
},
"formatMarkdownTable": {
"entrypoint": "node/mod.ts",
"id": "formatMarkdownTable",
"name": "formatMarkdownTable",
"content": "Given a set of `records` with known `columns`, format them into a pretty markdown table using the order from `columns`.\nIf a record does not have a specified value (it is null or undefined) it will be replaced with the `fallback` value.\n\n```js\nconst table = formatMarkdownTable(\n\t[\n\t\t{ name: 'Geoff Testington', age: 42 },\n\t\t{ name: \"Jess Smith\", age: 32 },\n\t\t{ name: \"Tyler Rockwell\" },\n\t],\n\t['name', 'age'],\n\t'~'\n)\n```\n\nWhich will generate:\n\n```\n| name | age |\n| ---------------- | --- |\n| Geoff Testington | 42 |\n| Jess Smith | 32 |\n| Tyler Rockwell | ~ |\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"loader": {
"entrypoint": "node/mod.ts",
"id": "loader",
"name": "loader",
"content": "`loader` let's you memoize the result of a function to create a singleton from it.\nIt works synchronously or with promises.\n\n```js\nlet index = 1\nconst useMessage = loader(() = 'hello there ${i++}')\n\nuseMessage() // hello there 1\nuseMessage() // hello there 1\nuseMessage() // hello there 1\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"trimIndentation": {
"entrypoint": "node/mod.ts",
"id": "trimIndentation",
"name": "trimIndentation",
"content": "`trimIndentation` takes a template literal (with values) and takes out the common whitespace.\nVery heavily based on [dedent](https://github.com/dmnd/dedent/tree/main)\n\n```js\nimport { trimIndentation } from \"gruber\";\n\nconsole.log(\n\ttrimIndentation`\n\t\tHello there!\n\t\tMy name is Geoff\n\t`,\n);\n```\n\nWhich will output this, without any extra whitespace:\n\n```\nHello there!\nMy name is Geoff\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
},
"reconstructTemplateString": {
"entrypoint": "node/mod.ts",
"id": "reconstructTemplateString",
"name": "reconstructTemplateString",
"content": "Turn arguments from a string template literal back into a string\n\n```js\n// 'I have 2 dogs'\nreconstructTemplateString(['I have ', ' dogs'], 2)\n```\n\nor via template tags\n\n```js\n// 'I have 2 dogs'\nreconstructTemplateString`I have ${2} dogs`\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
},
"preventExtraction": {
"entrypoint": "node/mod.ts",
"id": "preventExtraction",
"name": "preventExtraction",
"content": "Take steps to prevent an object from being extracted from the app,\ninspired by crypto.subtle.importKey's [extractable](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#extractable) parameter.\n\nThis will:\n- throw an error if the value are passed to JSON.stringify\n- it recursively applies to nested objects, arrays and items within arrays\n- [seal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal) and [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) the value and all nested objects & arrays\n\n```js\nconst config = preventExtraction({\n\tname: \"Geoff Testington\",\n\tpets: [\n\t\t{ name: \"Hugo\" },\n\t\t{ name: \"Helga\" },\n\t],\n favourite: {\n\t\tmountain: \"Cheviot\"\n\t}\n})\n\n// Any attempt to JSON-ify will result in an error\nconsole.log(JSON.stringify(config)) // throws a TypeError\nconsole.log(JSON.stringify(config.pets)) // throws a TypeError\nconsole.log(JSON.stringify(config.pets[0])) // throws a TypeError\nconsole.log(JSON.stringify(config.pets[1])) // throws a TypeError\nconsole.log(JSON.stringify(config.favourite)) // throws a TypeError\n```\n\nThe value will also be frozen and sealed, so any properties cannot be added, removed or modified.",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"dangerouslyExpose": {
"entrypoint": "node/mod.ts",
"id": "dangerouslyExpose",
"name": "dangerouslyExpose",
"content": "**DANGER** undo a [preventExtraction](#preventextraction) to allow values to be exposed.\nThis removes all of the precations that `preventExtraction` add.\n\n```js\nconsole.log(\n\tJSON.stringify(\n\t\tdangerouslyExpose(appConfig.meta)\n\t)\n)\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"pickProperties": {
"entrypoint": "node/mod.ts",
"id": "pickProperties",
"name": "pickProperties",
"content": "Create a subset of an object by picking off specific keys\n\n```js\nconst object = {\n name: \"Geoff Testington\",\n age: 42,\n pets: [\"Hugo\", \"Florence\"]\n}\npickProperties(object, [\"name\", \"age\"])\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getOrInsert": {
"entrypoint": "node/mod.ts",
"id": "getOrInsert",
"name": "getOrInsert",
"content": "Polyfil for [Map#getOrInsert](https://github.com/tc39/proposal-upsert)\n\n```js\nlet preferences = new Map()\nlet darkMode = getOrInsert(preferences, \"use_dark_mode\", true)\n\nlet groups = new Map()\nfor (let value of array) {\n getOrInsert(groups, value.theme, []).push(value)\n}\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"PromiseList": {
"entrypoint": "node/mod.ts",
"id": "PromiseList",
"name": "PromiseList",
"content": "A dynamic list of promises that are automatically removed when they resolve\n\n```js\nconst list = new PromiseList()\n\n// Add a promise that waits for 5 seconds\nlist.push(async () => {\n\tawait new Promise(r => setTimeout(r, 5_000))\n\n\t// Add dependant promises too\n\tlist.push(async () => {\n\t\tawait somethingElse()\n\t})\n})\n\n// Wait for all promises and dependants to resolve in one go\nawait promises.all()\n\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {
"push": {
"id": "PromiseList#push",
"name": "push",
"content": "Add a promise to the list using a factory method,\nthe `factory` just needs to return a promise\n\n```js\nlist.push(async () => {\n // ...\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"all": {
"id": "PromiseList#all",
"name": "all",
"content": "Wait for all promises to be resolved using `Promise.all`.\nIf new promises are added as a result of waiting, they are also awaited.\n\n```js\nawait list.all()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"length": {
"id": "PromiseList#length",
"name": "length",
"content": "Get the current number of promises in the list\n\n```js\nlist.length // 5\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"includesScope": {
"entrypoint": "node/mod.ts",
"id": "includesScope",
"name": "includesScope",
"content": "Check whether a provided scope meets the requirement of the expected scope\n\nThe idea is that a parent scope contains all children scopes, recursively.\nSo if you find all the parents of a given scope, you can test it against a scope that has been provided by a user.\n\nFor example `user:books:read` will match against:\n- `user:books:read`\n- `user:books`\n- `user`\n\nSo if any of those scopes are authorized, access can be granted.\n\n```js\nincludesScope(\"user:books:read\", \"user:books:read\"); // true\nincludesScope(\"user:books\", \"user:books:read\"); // true\nincludesScope(\"user\", \"user:books:read\"); // true\nincludesScope(\"user\", \"user:podcasts\"); // true\nincludesScope(\"user:books\", \"user:podcasts\"); // false\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Cors": {
"entrypoint": "node/mod.ts",
"id": "Cors",
"name": "Cors",
"content": "A development utility for apply CORS headers to a HTTP server using standard\n[Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)\nand [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects.\n\n> This really **should not** be used in production, I built this with the intention that\n> whatever reverse-proxy the app is deployed behind would manage these headers instead.\n\nThis implementation was adapted from [expressjs/cors](https://github.com/expressjs/cors),\nmostly to modernise it and remove features that weren't needed for this development-intended class.\n\nIt will:\n\n- set `Access-Control-Allow-Methods` to all methods\n- mirror headers in `Access-Control-Request-Headers` from the request\n- properly set the `Vary` header for any request header that varies the response\n- set `Access-Control-Allow-Origin` based on the `options.origins` option, allowing the origin if it is in the array or if the array includes `*`\n- set `Access-Control-Allow-Credentials` if opted in through `options.credentials`\n\n```js\nconst cors = new Cors({\n origins: ['http://localhost:8080'],\n credentials: true\n})\n\nconst request = new Request('http://localhost:3000/books/')\nconst response = Response.json({})\n\nconst result = cors.apply(request, response)\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"defineRoute": {
"entrypoint": "node/mod.ts",
"id": "defineRoute",
"name": "defineRoute",
"content": "`defineRoute` is the way of specifying how your server handles a specific bit of web traffic.\nIt returns the RouteDefinition which can be passed around and used in various places.\nMainly it is passed to a `FetchRouter` to serve web requests.\n\n```js\nexport const helloRoute = defineRoute({\n\tmethod: \"GET\",\n\tpathname: \"/hello/:name\",\n\thandler({ request, url, params }) {\n\t\treturn new Response(`Hello, ${params.name}!`);\n\t}\n})\n```",
"tags": {
"group": "Routing"
},
"children": {}
},
"FetchRouter": {
"entrypoint": "node/mod.ts",
"id": "FetchRouter",
"name": "FetchRouter",
"content": "`FetchRouter` is a web-native router for routes defined with `defineRoute`.\n\n```js\nconst routes = [defineRoute(\"...\"), defineRoute(\"...\"), defineRoute(\"...\")];\n\nconst router = new FetchRouter({ routes });\n```\n\nAll options to the `FetchRouter` constructor are optional\nand you can create a router without any options if you want.\n\n`routes` are the route definitions you want the router to processes,\nthe router will handle a request based on the first route that matches.\nSo order is important.\n\n`errorHandler(error, request)` is called if a 5xx `HTTPError` is caught, including unknown errors.\nIt is called with the offending error and the request it is associated with.\n\n> NOTE: The `errorHandler` could do more in the future,\n> like create it's own Response or mutate the existing response.\n> This has not been designed and is left open to future development if it becomes important.\n\n`log` is an **unstable** option to turn on HTTP logging, it can be a boolean or middleware function. It also logs HTTP errors if not already configured through `errorHandler`.\n\n`cors` is an **unstable** option to apply a [CORS](#cors) instance to all requests and adds an `OPTIONS` route handler",
"tags": {
"group": "Routing"
},
"children": {
"findMatches": {
"id": "FetchRouter#findMatches",
"name": "findMatches",
"content": "Find each matching route in turn\n\n```js\nlet request = new Request('...')\n\nfor (const match of router.findMatches(request)) {\n\t// do something with the request and/or break the loop\n}\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"processMatches": {
"id": "FetchRouter#processMatches",
"name": "processMatches",
"content": "Take an iterator of route matches and convert them into a HTTP Response\nby executing the route's handler.\nIt will return the first route to return a `Response` object\nor throw a `HTTPError` if no routes matched.\n\n```js\nconst response = await router.processMatches(request, matches)\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
},
"processRoute": {
"id": "FetchRouter#processRoute",
"name": "processRoute",
"content": "Execute a route's handler to generate a HTTP `Response`",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"handleError": {
"id": "FetchRouter#handleError",
"name": "handleError",
"content": "Attempt to handle an error thrown from a route's handler,\nchecking for well-known HTTPError instance or converting unknown errors into one.\nThe HTTPError is then used to convert the error into a HTTP `Response`.\n\nIf the error is server-based it will trigger the `FetchRouter`'s `errorHandler`.\n\n```js\nconst response = router.handleError(request, new Error(\"Something went wrong\"))\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getResponse": {
"id": "FetchRouter#getResponse",
"name": "getResponse",
"content": "Process all routes and get a HTTP Response.\n\n```js\nconst response = router.getResponse(\n\tnew Request('http://localhost/pathname')\n)\n```\n\n> NOTE: it would be nice to align this with the Fetch API `fetch` method signature.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"HTTPError": {
"entrypoint": "node/mod.ts",
"id": "HTTPError",
"name": "HTTPError",
"content": "A custom [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)\nsubclass that represents an HTTP error to be returned to the user.\n\nThis allows routes to throw specific HTTP errors directly and\n[FetchRouter](#fetchrouter) knows how to handle them and turn them into HTTP Responses\n\n\n\nYou can use well-known errors like below, you can also pass a [BodyInit](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#body) to customise the response body.\n\n```js\nthrow HTTPError.badRequest()\nthrow HTTPError.unauthorized()\nthrow HTTPError.notFound()\nthrow HTTPError.internalServerError()\nthrow HTTPError.notImplemented()\n\n// The plan is to add more error well-known codes as they are needed\n```\n\nYou can also manually construct the error:\n\n```js\nconst teapot = new HTTPError(418, \"I'm a teapot\");\n```",
"tags": {
"group": "HTTPError"
},
"children": {
"status": {
"id": "HTTPError#status",
"name": "status",
"content": "The HTTP status to return ~ [status](https://developer.mozilla.org/en-US/docs/Web/API/Response/status)",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"statusText": {
"id": "HTTPError#statusText",
"name": "statusText",
"content": "The status text to return ~ [statusText](https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText)",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"body": {
"id": "HTTPError#body",
"name": "body",
"content": "A custom body to send to the client",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"headers": {
"id": "HTTPError#headers",
"name": "headers",
"content": "Extra headers to send to the client",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"toResponse": {
"id": "HTTPError#toResponse",
"name": "toResponse",
"content": "Convert the HTTPError into a HTTP `Response` object\ntaking into account the `status`, `statusText` and `headers` fields on the error.\n\n```js\nconst error = new HTTPError(418, \"I'm a teapot\");\n\nerror.toResponse() // Response\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"HTTPError.badRequest": {
"id": "HTTPError.badRequest",
"name": "badRequest",
"content": "[400 Bad Request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400)\n\n```js\nthrow HTTPError.badRequest()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"HTTPError.unauthorized": {
"id": "HTTPError.unauthorized",
"name": "unauthorized",
"content": "[401 Unauthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401)\n\n```js\nthrow HTTPError.unauthorized()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"HTTPError.notFound": {
"id": "HTTPError.notFound",
"name": "notFound",
"content": "[404 Not Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404)\n\n```js\nthrow HTTPError.notFound()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"HTTPError.internalServerError": {
"id": "HTTPError.internalServerError",
"name": "internalServerError",
"content": "[500 Internal Server Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500)\n\n```js\nthrow HTTPError.internalServerError()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"HTTPError.notImplemented": {
"id": "HTTPError.notImplemented",
"name": "notImplemented",
"content": "[500 Not Implemented](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501)\n\n```js\nthrow HTTPError.notImplemented()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"getRequestBody": {
"entrypoint": "node/mod.ts",
"id": "getRequestBody",
"name": "getRequestBody",
"content": "Get and parse well-known request bodies based on the Content-Type header supplied\n\n```js\n\n// Parse a application/x-www-form-urlencoded\n// or multipart/form-data request\nconst formData = await getRequestBody(\n new Request('http://localhost:8000', { body: new FormData() })\n)\n\n// Parse an application/json request\nconst json = await getRequestBody(\n new Request('http://localhost:8000', {\n body: JSON.stringify({ hello: 'world' }),\n headers: { 'Content-Type': 'application/json' },\n })\n)\n```",
"tags": {
"unstable": "true",
"group": "Validation"
},
"children": {}
},
"assertRequestBody": {
"entrypoint": "node/mod.ts",
"id": "assertRequestBody",
"name": "assertRequestBody",
"content": "Validate the body of a request against a [StandardSchema](https://standardschema.dev/) or `Structure`.\nThis will throw nice [HTTPError](#httperror) errors that are caught by gruber and sent along to the user.\n\n```js\nconst struct = Structure.object({ name: Structure.string() })\n\nconst body1 = await assertRequestBody(struct, new Request('…'))\n```\n\n> **NOTE** — you need to await the function when passing a `Request`\n\nor from a JavaScript value:\n\n```js\nconst body2 = assertRequestBody(struct, { … })\nconst body3 = assertRequestBody(struct, new FormData(…))\nconst body3 = assertRequestBody(struct, new URLSearchParams(…))\n```\n\nyou can use any StandardSchema library with this:\n\n```js\nimport { z } from 'zod'\n\nconst body4 = assertRequestBody(\n z.object({ name: z.string() }),\n { name: \"Geoff Testington\" }\n)\n```",
"tags": {
"unstable": "true",
"group": "Validation"
},
"children": {}
},
"ServerSentEventMessage": {
"entrypoint": "node/mod.ts",
"id": "ServerSentEventMessage",
"name": "ServerSentEventMessage",
"content": "Represents a message in the [Server-Sent Event protocol](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields)\n\n```js\n// All fields are optional\nconst message = {\n comment: 'hello there',\n event: 'my-event',\n data: JSON.stringify({ lots: 'of', things: true }),\n id: 42,\n retry: 3600\n}\n```",
"tags": {
"group": "Miscellaneous",
"type": "true"
},
"children": {
"comment": {
"id": "ServerSentEventMessage#comment",
"name": "comment",
"content": "Ignored by the client, can be used to prevent connections from timing out",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"event": {
"id": "ServerSentEventMessage#event",
"name": "event",
"content": "A string identifying the type of event described. If specified this event is triggered, otherwise a \"message\" will be dispatched.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"data": {
"id": "ServerSentEventMessage#data",
"name": "data",
"content": "The data field for the message. Split by new lines.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"id": {
"id": "ServerSentEventMessage#id",
"name": "id",
"content": "The event ID to set the `EventSource` object's last event ID value.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"retry": {
"id": "ServerSentEventMessage#retry",
"name": "retry",
"content": "The reconnection time. If the connection to the server is lost, the browser will wait for the specified time before attempting to reconnect.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"ServerSentEventStream": {
"entrypoint": "node/mod.ts",
"id": "ServerSentEventStream",
"name": "ServerSentEventStream",
"content": "Transforms server-sent message objects into strings for the client.\n[more info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events).\n\nYou can then write [ServerSentEventMessage](#serversenteventmessage) to that stream over time.\n\n```js\nconst data = [{ data: \"hello there\" }]\n\n// Get a stream somehow, then pipe it through\nconst stream = ReadableStream.from(data)\n .pipeThrough(new ServerSentEventStream());\n\nconst response = new Response(stream, {\n headers: {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache\",\n },\n});\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getPostgresMigrations": {
"entrypoint": "node/mod.ts",
"id": "getPostgresMigrations",
"name": "getPostgresMigrations",
"content": "Query the postgres database to find migrations that have already been performed.\nReturning an array of `PostgresMigrationRecord`.\n\n```js\nconst sql // SqlDependency\nconst records = await getPostgresMigrations(sql)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"executePostgresMigration": {
"entrypoint": "node/mod.ts",
"id": "executePostgresMigration",
"name": "executePostgresMigration",
"content": "Perform either the **up** or **down** postgres migration and record what happened.\nThis will first start a transaction, so if anything goes wrong the whole operation is aborted.\nWithin the transaction, it attempts the run the action (either **up** or **down**) as specified.\n\nAfter the action is ran, it will follow up with updating the migration records.\nFor an **up** action, it will create a new `PostgresMigrationRecord`\nand insert it into the database.\nFor a **down** action, it will remove the corresponding `PostgresMigrationRecord`.\n\n> There is an edge case where it will not remove the record if running the `postgresBootstrapMigration` action,\n> because that migration deletes the migration table itself so would be pointless.\n\n```js\nconst sql // SqlDependency\nconst definition = definePostgresMigration(...)\n\nawait executePostgresMigration(definition, \"up\", sql)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"PostgresMigrationRecord": {
"entrypoint": "node/mod.ts",
"id": "PostgresMigrationRecord",
"name": "PostgresMigrationRecord",
"content": "A record in a postgres database containing information about a migration that has been run.\n\n```js\nconst record = {\n name: '001-add-users-table.js',\n created: new Date()\n}\n```",
"tags": {
"group": "Miscellaneous",
"type": "true"
},
"children": {}
},
"postgresBootstrapMigration": {
"entrypoint": "node/mod.ts",
"id": "postgresBootstrapMigration",
"name": "postgresBootstrapMigration",
"content": "This is a `MigrationDefinition` to bootstrap postgres migrations.\nIt sets up the initial \"migrations\" table that all other\nmigrations will be recorded in.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"definePostgresMigration": {
"entrypoint": "node/mod.ts",
"id": "definePostgresMigration",
"name": "definePostgresMigration",
"content": "A typed version of `defineMigration` that specalizes for a ` PostgresService`.\nThis is mostly useful to get a strongly typed `sql` parameter.\n\n```js\nimport { definePostgresMigration } from \"gruber\"\n\nexport default definePostgresMigration({\n async up(sql) {\n await sql.execute`\n CREATE TABLE users ...\n `\n },\n async down(sql) {\n await sql.execute`\n DROP TABLE users\n `\n }\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"PostgresClient": {
"entrypoint": "node/mod.ts",
"id": "PostgresClient",
"name": "PostgresClient",
"content": "Something that manages a connection to a postgres database and performs queries & transactions\n\n```js\nconst pg = {\n execute(strings, ...values) {},\n transaction() {},\n dispose() {},\n [Symbol.asyncDispose]() {}\n}\n```",
"tags": {
"group": "Miscellaneous",
"type": "true"
},
"children": {}
}
}