Migrating to v2 of the Node.js SDK

The v2 release of the Node.js SDK represents a significant rethinking of the developer experience. The biggest new feature — generating TypeScript types from your Polar policy — isn't actually a breaking change, but there are a few other breaking changes to be aware of.

Minimum supported Node.js version: 16

The Node.js SDK now requires Node.js 16 or later.

Centralized Authorization Data API

The Centralized Authorization Data API has been condensed from 6 methods to 4.

tell to insert

To migrate from tell to insert, wrap the arguments in an array:


const user = { type: "User", id: "1" };
const repo = { type: "Repo", id: "2" };
-await oso.tell("has_role", user, "member", repo);
+await oso.insert(["has_role", user, "member", repo]);

This new array-wrapped syntax matches the representation of facts across the rest of the API.

delete

To migrate from the previous delete API, wrap the arguments in an array:


const user = { type: "User", id: "1" };
const repo = { type: "Repo", id: "2" };
-await oso.delete("has_role", user, "member", repo);
+await oso.delete(["has_role", user, "member", repo]);

This new array-wrapped syntax matches the representation of facts across the rest of the API.

Additionally, the new delete method supports deleting all facts matching a pattern:


const user = { type: "User", id: "1" };
// Remove all of User:1's roles across the entire system.
await oso.delete(["has_role", user, null, null]);

get

To migrate from the previous get API, wrap the arguments in an array:


const user = { type: "User", id: "1" };
-const roles = await oso.get("has_role", user, null, null);
+const roles = await oso.get(["has_role", user, null, null]);

bulk to batch or delete

To migrate from bulk to batch, turn all patterns to delete into calls to tx.delete() and all facts to insert into calls to tx.insert():


const user = { type: "User", id: "1" };
const repo = { type: "Repo", id: "3" };
-await oso.bulk(
- [["has_role", user, null, null]],
- [["has_role", user, "member", repo]],
-);
+await oso.batch((tx) => {
+ tx.delete(["has_role", user, null, null]);
+ tx.insert(["has_role", user, "member", repo]);
+});

Additionally, the delete API now handles deleting many facts at once via wildcards. If you were previously using the bulk API just to delete many facts at once, you can now use the delete API directly without wrapping it in a call to batch:


const user = { type: "User", id: "1" };
-await oso.bulk(
- [["has_role", user, null, null]],
-);
+await oso.delete(["has_role", user, null, null]);

bulkTell to batch

To migrate from bulkTell to batch, turn all facts to insert into calls to tx.insert():


const facts = [
["has_role", { type: "User", id: "1" }, "member", { type: "Repo", id: "2" }],
["has_role", { type: "User", id: "1" }, "member", { type: "Repo", id: "3" }],
];
-await oso.bulkTell(facts);
+await oso.batch((tx) => facts.forEach((f) => tx.insert(f)));

bulkDelete to batch

To migrate from bulkDelete to batch, turn all facts to delete into calls to tx.delete():


const facts = [
["has_role", { type: "User", id: "1" }, "member", { type: "Repo", id: "2" }],
["has_role", { type: "User", id: "1" }, "member", { type: "Repo", id: "3" }],
];
-await oso.bulkDelete(facts);
+await oso.batch((tx) => facts.forEach((f) => tx.delete(f)));

Additionally, the new batch method supports deleting all facts matching a pattern:


const user1 = { type: "User", id: "1" };
const user2 = { type: "User", id: "2" };
// Remove all roles for User:1 and User:2 across the entire system.
await oso.batch((tx) => {
tx.delete(["has_role", user1, null, null]);
tx.delete(["has_role", user2, null, null]);
});

Query API

We've replaced the Query API with a more powerful and flexible QueryBuilder API with a fluent interface. We've also dropped the AuthorizeResources and BulkActions APIs in favor of the QueryBuilder.

query to buildQuery

We recommend taking a look at the reference docs for the new QueryBuilder API. It's more flexible and expressive than the old Query API, and it may let you simplify your application code.

But if you just want to migrate your existing queries as-is, here's how.

Queries with no wildcards

Typically, a query with no wildcards is used to check for the existence of a derived rule or fact.

With the old Query API:


const results = await oso.query(
"has_role",
{ type: "User", id: "bob" },
"reader",
{ type: "Repository", id: "acme" }
);
// => [["has_role", {type: "User", id: "bob"}, "reader", {type: "Repository", id: "acme"}]]
const ok = results.length > 0;
// true

With the new QueryBuilder API:


const ok = await oso
.buildQuery([
"has_role",
{ type: "User", id: "bob" },
"reader",
{ type: "Repository", id: "acme" },
])
.evaluate(); // Return a boolean
// => true

Queries with type-constrained wildcards

The old Query API let you query for all the results of a particular type. To migrate these, replace type-constrained wildcards with a typedVar(myType) variable.

With the old Query API:


// Query for all the repos `User:bob` can `read`
await oso.query("allow", { type: "User", id: "bob" }, "read", {
type: "Repository",
});
// => [
// ["allow", {type: "User", id: "bob"}, "read", {type: "Repository", id: "acme"}],
// ["allow", {type: "User", id: "bob"}, "read", {type: "Repository", id: "anvils"}],
// ]

With the new QueryBuilder API:


const repos = typedVar("Repository");
await oso
.buildQuery(["allow", { type: "User", id: "bob" }, "read", repos])
.evaluate(repos); // Return just the IDs of the repos bob can read
// => ["acme", "anvils"]

If you have several type-constrained wildcards in a single query, you may prefer to get results as a map:


const users = typedVar("User");
const repos = typedVar("Repository");
await oso
.buildQuery([
// Query for which users can read which repos
"allow",
users,
"read",
repos,
])
// Return the results as a map from user IDs to arrays of repo IDs
.evaluate(new Map([[users, repos]]));
// => new Map(Object.entries({ "bob": ["acme", "anvil"], "alice": ["anvil"], ... }))

Queries with unconstrained wildcards

The old Query API let you use null to query for many types of results at once:


// Query for all the objects `User:bob` can `read`
await oso.query("allow", { type: "User", id: "bob" }, "read", null);
// => [
// ["allow", {type: "User", id: "bob"}, "read", {type: "Repository", id: "acme"}],
// ["allow", {type: "User", id: "bob"}, "read", {type: "Repository", id: "anvil"}],
// ["allow", {type: "User", id: "bob"}, "read", {type: "Issue", id: "123"}],
// ...
// ]

In the new QueryBuilder API, this is no longer possible.

Instead, make one request for each concrete type:


let readableTypes = ["Repository", "Issue", "Organization"];
await Promise.all(
readableTypes.map((typeName) => {
const resource = typedVar(typeName);
return [
typeName,
oso
.buildQuery(["allow", { type: "User", id: "admin" }, "read", resource])
.evaluate(resource), // Return an array of resource IDs
];
})
);
// [["Repository", ["acme", "anvil"]], ["Issue", ["123"]], ...]

Handling wildcards in results

The old Query API sometimes returned results containing the value null, indicating that any value could apply at a particular position. For example:


// Query for all the objects `User:admin` can `read`
await oso.query("allow", { type: "User", id: "admin" }, "read", null);
// => [
// // `User:admin` can `read` anything
// ["allow", {type: "User", id: "admin"}, "read", null],
// ]

The new QueryBuilder API will instead return the string "*" to mean the same thing, just like the Check APIs-


const repos = typedVar("Repository");
await oso
.buildQuery(["allow", { type: "User", id: "admin" }, "read", repos])
.evaluate(repos); // Return just the IDs of the repos admin can read
// => ["*"] // admin can read anything

Context facts

If you are using context facts with query, check out the withContextFacts QueryBuilder method.

authorizeResources to buildQuery

The authorizeResources API was used to authorize many resources for a single actor and action.


const user = { type: "User", id: "1" };
const repoIds = ["acme", "anvil"];
const repos = repoIds.map((id) => {
type: "Repo", id;
});
await oso.authorizeResources(user, "read", repos);
// => ["acme"]

With the new QueryBuilder API:


const user = { type: "User", id: "1" };
const repoIds = ["acme", "anvil"];
const repoVar = typedVar("Repo");
await oso
.buildQuery(["allow", user, "read", repoVar])
.in(repoVar, repoIds)
.evaluate(repoVar);
// => ["acme"]

If you are using context facts with authorizedResources, check out the withContextFacts QueryBuilder method.

bulkActions to buildQuery

The bulkActions API was used to get the actions an actor can take on a list of resources of a single type:


const user = { type: "User", id: "1" };
const repoIds = ["acme", "anvil"];
const repos = repoIds.map((id) => {
type: "Repo", id;
});
await oso.bulkActions(user, repos);
// => [["read"], []]

The result is an in-order list, which must be mapped back to the initial resource list.

With the new QueryBuilder API, you can get this mapping returned more directly:


const user = { type: "User", id: "1" };
const repoIds = ["acme", "anvil"];
const actionVar = typedVar("String");
const repoVar = typedVar("Repo");
await oso
.buildQuery(["allow", user, actionVar, repoVar])
.in(repoVar, repoIds)
.evaluate(new Map([[repoVar, actionVar]]));
// => new Map(Object.entries({ "acme": ["read"] }))

Note that resources which the actor has no actions on are omitted from the results.

You can even get the actions an actor can take on ALL resources by leaving out the in clause:


const user = { type: "User", id: "1" };
const repoIds = ["acme", "anvil"];
const actionVar = typedVar("String");
const repoVar = typedVar("Repo");
await oso
.buildQuery(["allow", user, actionVar, repoVar])
.evaluate(new Map([[repoVar, actionVar]]));
// => new Map(Object.entries({
// "acme": ["read"],
// "boulder": ["read"],
// ...
// }))

Exported TypeScript types

Instance to IntoValue

The Instance type has been removed. If your application code refers to this type, you should replace it with IntoValue.