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
.