Protected queries

Learn how to define and use protected queries with Kilpi.$query for co-located authorization logic.


You can protect queries in any way you’d like, however Kilpi offers protected queries created using Kilpi.$query as a solution to co-locate your queries and authorization logic.

Defining a protected query

To define a protected query, pass the query function as the first argument to Kilpi.$query. As the second argument, provide an object with the authorize method. The authorize function receives the input to the query as well as its output and the current subject. It then returns the output to which the subject is authorized.

const getPost = Kilpi.$query(
// Query function
async (id: string) => {
return await db.posts.findById(id);
},
// Separate but co-located authorization which runs after the query
{
async authorize({ input: [id], output: post, subject }) {
if (!post) return null;
const { granted } = await Kilpi.posts.read(post).authorize();
if (!granted) return null;
return post;
},
},
);

You call the protected query using the authorized method as follows.

// Post or null
const post = await getPost.authorized("123");

Redacting data

A common issue with authorization is redacting data in a type-safe way. This is easy with the Kilpi.$query API.

const getUserDetails = Kilpi.$query(
async (userId: string) => {
return await db.users.findById(userId);
},
{
async authorize({ output: user }) {
if (!user) return null;
const { granted } = await Kilpi.users.readPrivate(user).authorize();
// Unauthorized: Only return public fields
if (!granted) return { userId: user.id, name: user.name };
// Authorized: Return also private fields
return { userId: user.id, name: user.name, email: user.email };
},
},
);
// TS knows email is optional, as it may be redacted away
const userDetails = await getUserDetails.authorized(userId);
userDetails.email; // string | undefined

Throwing on unauthorized

Commonly, instead of return null you want to throw and stop execution on unauthorized. Using the assert() API (or throwing your own exceptions) works very well with protected queries.

const getPost = Kilpi.$query(..., {
async authorize({ output: post }) {
// Throws on unauthorized
if (post) await Kilpi.posts.read(post).authorize().assert();
return post;
}
});