Skip to content

Commit

Permalink
📦 NEW: checkAllSettle (#2)
Browse files Browse the repository at this point in the history
* 📦 NEW: checkAllSettle

* add test
  • Loading branch information
rphlmr authored Sep 30, 2024
1 parent 4b299fe commit f52397d
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 5 deletions.
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Finally, if the condition is a type guard, the parameter you pass will be inferr
To define policies, you create a policy set using the `definePolicies` function.
Each policy definition is created using the `definePolicy` function, which takes a policy name and a callback that defines the policy logic (or a boolean value).

> [!IMPORTANT]
> [!IMPORTANT]
> For convenience, the condition can be a boolean value but **you will lose type inference**
>
> If you want TS to infer something (not null, union, etc), use a condition function
Expand Down Expand Up @@ -393,6 +393,56 @@ if (check(guard.post.policy("post has comments"), post)) {
console.log("Post has comments");
}
```
### `checkAllSettle`
Evaluates all the policies with `check` and returns a snapshot with the results.
It's useful to serialize policies.
```ts
export function checkAllSettle<TPolicies extends PolicyTuple[], TPolicyName extends TPolicies[number][0]["name"]>(
policies: TPolicies
): PoliciesSnapshot<TPolicyName>
```
Example:
```ts
// TLDR
const snapshot = checkAllSettle([
[guard.post.policy("my post"), post],
[guard.post.policy("all my published posts"), post],
]);

// Example
const PostPolicies = definePolicies((context: Context) => {
const myPostPolicy = definePolicy(
"my post",
(post: Post) => post.userId === context.userId,
() => new Error("Not the author")
);

return [
myPostPolicy,
definePolicy("all published posts or mine", (post: Post) =>
or(check(myPostPolicy, post), post.status === "published")
),
];
});

const guard = {
post: PostPolicies(context),
};

const snapshot = checkAllSettle([
[guard.post.policy("my post"), post],
[guard.post.policy("all my published posts"), post],
["post has comments", post.comments.length > 0],
]);

console.log(snapshot); // { "my post": boolean; "all my published posts": boolean; "post has comments": boolean; }
console.log(snapshot["my post"]) // boolean
```
### Condition helpers
#### `or`
Logical OR operator for policy conditions.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "comply",
"version": "0.2.0",
"version": "0.3.0",
"description": "Comply is a tiny library to help you define policies in your app",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
62 changes: 61 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { z } from "zod";
import { assert, and, check, definePolicies, definePolicy, matchSchema, notNull, or } from ".";
import { assert, and, check, checkAllSettle, definePolicies, definePolicy, matchSchema, notNull, or } from ".";

describe("Define policy", () => {
type Post = { userId: string; comments: string[] };
Expand Down Expand Up @@ -887,3 +887,63 @@ describe("Logical operators", () => {
).toThrowError();
});
});

describe("Check all settle", () => {
type Context = { userId: string };
type Post = { userId: string; comments: string[]; status: "published" | "draft" | "archived" };

it("should snapshot policies", () => {
const PostPolicies = definePolicies((context: Context) => {
const myPostPolicy = definePolicy(
"my post",
(post: Post) => post.userId === context.userId,
() => new Error("Not the author")
);

return [
myPostPolicy,
definePolicy("all my published posts", (post: Post) =>
and(check(myPostPolicy, post), post.status === "published")
),
];
});

const guard = {
post: PostPolicies({ userId: "1" }),
};

const snapshot = checkAllSettle([
[definePolicy("is not null", notNull), "not null"],
[definePolicy("is true", true)],
["post has comments", true],
["post has likes", () => true],
[guard.post.policy("my post"), { userId: "1", comments: [], status: "published" }],
[guard.post.policy("all my published posts"), { userId: "1", comments: [], status: "published" }],
]);

expect(snapshot).toStrictEqual({
"is not null": true,
"is true": true,
"post has comments": true,
"post has likes": true,
"my post": true,
"all my published posts": true,
});

expectTypeOf(snapshot).toEqualTypeOf<{
"is not null": boolean;
"is true": boolean;
"post has comments": boolean;
"post has likes": boolean;
"my post": boolean;
"all my published posts": boolean;
}>();

/** @ts-expect-error */
expectTypeOf(checkAllSettle([[definePolicy("is not null", notNull)]])).toEqualTypeOf<{ [x: string]: boolean }>();
/** @ts-expect-error */
expectTypeOf(checkAllSettle([[definePolicy("is true", true), "extra arg"]])).toEqualTypeOf<{
[x: string]: boolean;
}>();
});
});
87 changes: 85 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ type PolicySetOrFactory<T extends PoliciesOrFactory> = T extends AnyPolicies
? (...args: Parameters<T>) => PolicySet<ReturnType<T>>
: never;

type WithRequiredContext<T> = T extends (arg: infer A) => any ? (unknown extends A ? never : T) : never;
type WithRequiredArg<T> = T extends (arg: infer A) => any ? (unknown extends A ? never : T) : never;

/**
* Create a set of policies
Expand Down Expand Up @@ -222,7 +222,7 @@ export function definePolicies<T extends AnyPolicies>(policies: T): PolicySet<T>
* ```
*/
export function definePolicies<Context, T extends PoliciesOrFactory>(
define: WithRequiredContext<(context: Context) => T>
define: WithRequiredArg<(context: Context) => T>
): (context: Context) => PolicySetOrFactory<T>;

export function definePolicies<Context, T extends PoliciesOrFactory>(defineOrPolicies: T | ((context: Context) => T)) {
Expand Down Expand Up @@ -589,6 +589,89 @@ export function check<TPolicyCondition extends PolicyCondition>(
return typeof policy.condition === "boolean" ? policy.condition : policy.condition(arg);
}

type PolicyTuple =
| readonly [string, PolicyConditionNoArg]
| readonly [Policy<string, PolicyConditionNoArg>]
| readonly [Policy<string, PolicyConditionWithArg>, any];

type InferPolicyName<TPolicyTuple> = TPolicyTuple extends readonly [infer name, any]
? name extends Policy<infer Name, any>
? Name
: name extends string
? name
: never
: TPolicyTuple extends readonly [Policy<infer Name, any>]
? Name
: never;

type PoliciesSnapshot<TPolicyName extends string> = { [K in TPolicyName]: boolean };

/**
* Create a snapshot of policies and their evaluation results
*
* It evaluates all the policies with `check`
*
* @param policies - A tuple of policies and their arguments (if needed)
*
* @example
* ```ts
* // TLDR
const snapshot = checkAllSettle([
[guard.post.policy("my post"), post],
[guard.post.policy("all my published posts"), post],
["post has comments", post.comments.length > 0],
]);
// returns: { "my post": boolean; "all my published posts": boolean; "post has comments": boolean; }
* // Example
const PostPolicies = definePolicies((context: Context) => {
const myPostPolicy = definePolicy(
"my post",
(post: Post) => post.userId === context.userId,
() => new Error("Not the author")
);
return [
myPostPolicy,
definePolicy("all published posts or mine", (post: Post) =>
or(check(myPostPolicy, post), post.status === "published")
),
];
});
const guard = {
post: PostPolicies(context),
};
const snapshot = checkAllSettle([
[guard.post.policy("my post"), post],
[guard.post.policy("all my published posts"), post],
["post has comments", post.comments.length > 0],
]);
console.log(snapshot); // { "my post": boolean; "all my published posts": boolean; "post has comments": boolean; }
* ```
*/
export function checkAllSettle<
const TPolicies extends readonly PolicyTuple[],
TPolicyTuple extends TPolicies[number],
TPolicyName extends InferPolicyName<TPolicyTuple>,
>(policies: TPolicies): PoliciesSnapshot<TPolicyName> {
return policies.reduce(
(acc, policyTuple) => {
const [policyOrName, arg] = policyTuple;
const policyName = typeof policyOrName === "string" ? policyOrName : policyOrName.name;

acc[policyName as TPolicyName] =
typeof policyOrName === "string" ? (typeof arg === "function" ? arg() : arg) : policyOrName.check(arg);

return acc;
},
{} as PoliciesSnapshot<TPolicyName>
);
}

/* -------------------------------------------------------------------------- */
/* Helpers; */
/* -------------------------------------------------------------------------- */
Expand Down

0 comments on commit f52397d

Please sign in to comment.