Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

📦 NEW: checkAllSettle #2

Merged
merged 2 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading