Configuring requests
Configuring requests in Alette Signal is done by providing middleware to request blueprints.
Request blueprint
A request blueprint is a set of predefined request instructions, executed by the Alette Signal "core" system. When a request is finished, the promise returned from the .execute()
method is resolved or rejected:
await getPosts.execute();
Plugins in Alette Signal
Plugins in Alette Signal are modules defining and configuring request blueprints before exposing them for usage. On its own blueprint()
contains no middleware, and needs to be defined and configured by plugin authors using built-in middleware.
Alette Signal core plugin exposes 3 request blueprints out of the box:
- Query - preconfigured for
GET
HTTP requests. - Mutation - preconfigured for
POST
,PATCH
,DELETE
andPUT
HTTP requests. - Custom - used for executing dependent requests, or creating custom request behaviours by composing middleware.
import { coreApiPlugin } from '@alette/signal';
const core = coreApiPlugin();
const { query, mutation, custom } = core.use();
INFO
Alette Signal treats query()
, mutation()
and custom()
as middleware "black boxes", nothing more. The same is true for any blueprints plugin authors may define.
DANGER
A plugin must be activated for its request blueprints to work.
Request arguments
To define request arguments, provide the input()
middleware to a request blueprint:
import { input, as, coreApiPlugin } from '@alette/signal';
import * as z from 'zod';
const core = coreApiPlugin();
const { query } = core.use();
const myQuery = query(
input(z.object({ hey: z.string() }))
);
The input()
middleware accepts a Zod schema or any other runtime validation schema implementing the Standard Schema interface. To skip runtime validation, use the as()
type placeholder instead:
import { as, /* ... */ } from '@alette/signal';
const myQuery = query(
input(as<{ hey: string }>())
);
TIP
The as()
type placeholder implements the Standard Schema interface, making it possible to use it instead of runtime schemas like Zod.
DANGER
The as()
type placeholder does not validate provided types at runtime.
To pass arguments to request blueprints, use the args
property:
const response = await myQuery.execute({
args: { hey: 'Alette Signal' }
});
Request output
To define request output, provide the output()
middleware to a request blueprint:
import { output, /* ... */ } from '@alette/signal';
const myQuery = query(
/* ... */
output(z.object({ hello: z.string() })),
);
/*
* The "response" variable is now
* of the "{ hello: string }" type.
* */
const response = await myQuery.execute({
args: { hey: 'Alette Signal' }
});
The output()
middleware accepts a Zod schema or any other runtime validation schema implementing the Standard Schema interface. To skip runtime validation, use the as()
type placeholder instead:
import { as, /* ... */ } from '@alette/signal';
const myQuery = query(
output(as<{ hello: string }>()),
);
Request settings
Request settings are typed values required by middleware and accepted by the .execute()
method, or the execute()
function exposed by mounted requests:
await myQuery.execute(
// Request settings
{
args: { hey: 'Alette Signal' }
}
);
INFO
Request settings are dynamic based on middleware present in request blueprints.
Request setting binding
Request setting binding is a technique allowing request blueprints to reuse the same request settings for every request.
To bind request settings to request blueprints use the .using()
method:
const boundQuery = myQuery.using(() => ({
args: { hey: 'Alette Signal' }
}));
The function passed to the .using()
method is invoked on each request execution, allowing the request to keep request settings up-to-date:
let name = "Alette Signal 1";
const boundQuery = myQuery.using(() => ({
args: { hey: name }
}));
// Will use "Alette Signal 1"
await boundQuery.execute();
name = "Alette Signal 2";
// Will use "Alette Signal 2"
await boundQuery.execute();
TIP
You can think of the .using()
method as a variation of native JS .bind()
.
Now boundQuery
can be executed as is:
// Each invocation is using the same request settings
// { args: { hey: 'Alette Signal' } }
await boundQuery.execute();
await boundQuery.execute();
await boundQuery.execute();
To override bound settings, provide new request settings to .execute()
:
await boundQuery.execute({
args: { hey: 'Not Alette Signal?' }
});
DANGER
The .using()
method creates a new request blueprint with bound settings using myQuery()
as a foundation - it does not return a reference to myQuery()
back.
This can be validated by running a JS object reference check:
myQuery === boundQuery // returns false
WARNING
- When
.using()
is called, it prevents you from calling.with()
again or converting your blueprint to a factory. - This prevents situations where you add middleware like
input()
that
might make previously provided settings to.using()
invalid.
Request behaviour inheritance
To inherit request behaviour, call the .with()
method on a request blueprint:
import { input, output, as } from '@alette/signal';
// ...
const myQuery = query(
/* ... */
input(as<{ hey: string }>())
);
// Inherits all middleware from "myQuery"
const myQuery2 = myQuery.with(
output(as<{ welcome: string }>())
);
// Now TypeScript type of "response" is
// "{ welcome: string }", not "unknown".
const response = await myQuery2.execute({
args: { hey: 'Alette Signal' }
});
DANGER
The .with()
request blueprint method clones the original request blueprint together with its middleware and returns a new one. It does not return a reference to myQuery()
back, meaning myQuery()
and myQuery2()
are 2 different requests.
This can be validated by running a JS object reference check:
myQuery === myQuery2 // returns false
Request blueprint factory
Request blueprint factory is a function containing previously provided middleware, while accepting new middleware as arguments.
To turn request blueprints into factories, call the request blueprint .toFactory()
method.
const baseQuery = myQuery
.with(
output(as<{ welcome: string }>())
)
.toFactory()
Now, baseQuery()
has a preconfigured behaviour defined by the input()
and output()
middleware, while allowing other middleware to override it:
import { output, as } from '@alette/signal'
// ...
const queryFromBaseQuery = baseQuery(
output(as<{ overriddenOutput: true }>())
)
// Now TypeScript type of "response" is
// "{ overriddenOutput: true }", not "{ welcome: string }".
const response = await queryFromBaseQuery.execute({
args: { hey: 'Alette Signal' }
})
INFO
Under the hood, request blueprint factories use .with()
.