Skip to content

Commit f52397d

Browse files
authored
📦 NEW: checkAllSettle (#2)
* 📦 NEW: checkAllSettle * add test
1 parent 4b299fe commit f52397d

File tree

4 files changed

+198
-5
lines changed

4 files changed

+198
-5
lines changed

README.md

+51-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Finally, if the condition is a type guard, the parameter you pass will be inferr
8989
To define policies, you create a policy set using the `definePolicies` function.
9090
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).
9191

92-
> [!IMPORTANT]
92+
> [!IMPORTANT]
9393
> For convenience, the condition can be a boolean value but **you will lose type inference**
9494
>
9595
> If you want TS to infer something (not null, union, etc), use a condition function
@@ -393,6 +393,56 @@ if (check(guard.post.policy("post has comments"), post)) {
393393
console.log("Post has comments");
394394
}
395395
```
396+
397+
### `checkAllSettle`
398+
Evaluates all the policies with `check` and returns a snapshot with the results.
399+
400+
It's useful to serialize policies.
401+
402+
```ts
403+
export function checkAllSettle<TPolicies extends PolicyTuple[], TPolicyName extends TPolicies[number][0]["name"]>(
404+
policies: TPolicies
405+
): PoliciesSnapshot<TPolicyName>
406+
```
407+
408+
Example:
409+
```ts
410+
// TLDR
411+
const snapshot = checkAllSettle([
412+
[guard.post.policy("my post"), post],
413+
[guard.post.policy("all my published posts"), post],
414+
]);
415+
416+
// Example
417+
const PostPolicies = definePolicies((context: Context) => {
418+
const myPostPolicy = definePolicy(
419+
"my post",
420+
(post: Post) => post.userId === context.userId,
421+
() => new Error("Not the author")
422+
);
423+
424+
return [
425+
myPostPolicy,
426+
definePolicy("all published posts or mine", (post: Post) =>
427+
or(check(myPostPolicy, post), post.status === "published")
428+
),
429+
];
430+
});
431+
432+
const guard = {
433+
post: PostPolicies(context),
434+
};
435+
436+
const snapshot = checkAllSettle([
437+
[guard.post.policy("my post"), post],
438+
[guard.post.policy("all my published posts"), post],
439+
["post has comments", post.comments.length > 0],
440+
]);
441+
442+
console.log(snapshot); // { "my post": boolean; "all my published posts": boolean; "post has comments": boolean; }
443+
console.log(snapshot["my post"]) // boolean
444+
```
445+
396446
### Condition helpers
397447
#### `or`
398448
Logical OR operator for policy conditions.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "comply",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "Comply is a tiny library to help you define policies in your app",
55
"main": "./dist/index.js",
66
"module": "./dist/index.mjs",

src/index.test.ts

+61-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, expectTypeOf, it } from "vitest";
22
import { z } from "zod";
3-
import { assert, and, check, definePolicies, definePolicy, matchSchema, notNull, or } from ".";
3+
import { assert, and, check, checkAllSettle, definePolicies, definePolicy, matchSchema, notNull, or } from ".";
44

55
describe("Define policy", () => {
66
type Post = { userId: string; comments: string[] };
@@ -887,3 +887,63 @@ describe("Logical operators", () => {
887887
).toThrowError();
888888
});
889889
});
890+
891+
describe("Check all settle", () => {
892+
type Context = { userId: string };
893+
type Post = { userId: string; comments: string[]; status: "published" | "draft" | "archived" };
894+
895+
it("should snapshot policies", () => {
896+
const PostPolicies = definePolicies((context: Context) => {
897+
const myPostPolicy = definePolicy(
898+
"my post",
899+
(post: Post) => post.userId === context.userId,
900+
() => new Error("Not the author")
901+
);
902+
903+
return [
904+
myPostPolicy,
905+
definePolicy("all my published posts", (post: Post) =>
906+
and(check(myPostPolicy, post), post.status === "published")
907+
),
908+
];
909+
});
910+
911+
const guard = {
912+
post: PostPolicies({ userId: "1" }),
913+
};
914+
915+
const snapshot = checkAllSettle([
916+
[definePolicy("is not null", notNull), "not null"],
917+
[definePolicy("is true", true)],
918+
["post has comments", true],
919+
["post has likes", () => true],
920+
[guard.post.policy("my post"), { userId: "1", comments: [], status: "published" }],
921+
[guard.post.policy("all my published posts"), { userId: "1", comments: [], status: "published" }],
922+
]);
923+
924+
expect(snapshot).toStrictEqual({
925+
"is not null": true,
926+
"is true": true,
927+
"post has comments": true,
928+
"post has likes": true,
929+
"my post": true,
930+
"all my published posts": true,
931+
});
932+
933+
expectTypeOf(snapshot).toEqualTypeOf<{
934+
"is not null": boolean;
935+
"is true": boolean;
936+
"post has comments": boolean;
937+
"post has likes": boolean;
938+
"my post": boolean;
939+
"all my published posts": boolean;
940+
}>();
941+
942+
/** @ts-expect-error */
943+
expectTypeOf(checkAllSettle([[definePolicy("is not null", notNull)]])).toEqualTypeOf<{ [x: string]: boolean }>();
944+
/** @ts-expect-error */
945+
expectTypeOf(checkAllSettle([[definePolicy("is true", true), "extra arg"]])).toEqualTypeOf<{
946+
[x: string]: boolean;
947+
}>();
948+
});
949+
});

src/index.ts

+85-2
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ type PolicySetOrFactory<T extends PoliciesOrFactory> = T extends AnyPolicies
131131
? (...args: Parameters<T>) => PolicySet<ReturnType<T>>
132132
: never;
133133

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

136136
/**
137137
* Create a set of policies
@@ -222,7 +222,7 @@ export function definePolicies<T extends AnyPolicies>(policies: T): PolicySet<T>
222222
* ```
223223
*/
224224
export function definePolicies<Context, T extends PoliciesOrFactory>(
225-
define: WithRequiredContext<(context: Context) => T>
225+
define: WithRequiredArg<(context: Context) => T>
226226
): (context: Context) => PolicySetOrFactory<T>;
227227

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

592+
type PolicyTuple =
593+
| readonly [string, PolicyConditionNoArg]
594+
| readonly [Policy<string, PolicyConditionNoArg>]
595+
| readonly [Policy<string, PolicyConditionWithArg>, any];
596+
597+
type InferPolicyName<TPolicyTuple> = TPolicyTuple extends readonly [infer name, any]
598+
? name extends Policy<infer Name, any>
599+
? Name
600+
: name extends string
601+
? name
602+
: never
603+
: TPolicyTuple extends readonly [Policy<infer Name, any>]
604+
? Name
605+
: never;
606+
607+
type PoliciesSnapshot<TPolicyName extends string> = { [K in TPolicyName]: boolean };
608+
609+
/**
610+
* Create a snapshot of policies and their evaluation results
611+
*
612+
* It evaluates all the policies with `check`
613+
*
614+
* @param policies - A tuple of policies and their arguments (if needed)
615+
*
616+
* @example
617+
* ```ts
618+
* // TLDR
619+
const snapshot = checkAllSettle([
620+
[guard.post.policy("my post"), post],
621+
[guard.post.policy("all my published posts"), post],
622+
["post has comments", post.comments.length > 0],
623+
]);
624+
625+
// returns: { "my post": boolean; "all my published posts": boolean; "post has comments": boolean; }
626+
627+
* // Example
628+
const PostPolicies = definePolicies((context: Context) => {
629+
const myPostPolicy = definePolicy(
630+
"my post",
631+
(post: Post) => post.userId === context.userId,
632+
() => new Error("Not the author")
633+
);
634+
635+
return [
636+
myPostPolicy,
637+
definePolicy("all published posts or mine", (post: Post) =>
638+
or(check(myPostPolicy, post), post.status === "published")
639+
),
640+
];
641+
});
642+
643+
const guard = {
644+
post: PostPolicies(context),
645+
};
646+
647+
const snapshot = checkAllSettle([
648+
[guard.post.policy("my post"), post],
649+
[guard.post.policy("all my published posts"), post],
650+
["post has comments", post.comments.length > 0],
651+
]);
652+
653+
console.log(snapshot); // { "my post": boolean; "all my published posts": boolean; "post has comments": boolean; }
654+
* ```
655+
*/
656+
export function checkAllSettle<
657+
const TPolicies extends readonly PolicyTuple[],
658+
TPolicyTuple extends TPolicies[number],
659+
TPolicyName extends InferPolicyName<TPolicyTuple>,
660+
>(policies: TPolicies): PoliciesSnapshot<TPolicyName> {
661+
return policies.reduce(
662+
(acc, policyTuple) => {
663+
const [policyOrName, arg] = policyTuple;
664+
const policyName = typeof policyOrName === "string" ? policyOrName : policyOrName.name;
665+
666+
acc[policyName as TPolicyName] =
667+
typeof policyOrName === "string" ? (typeof arg === "function" ? arg() : arg) : policyOrName.check(arg);
668+
669+
return acc;
670+
},
671+
{} as PoliciesSnapshot<TPolicyName>
672+
);
673+
}
674+
592675
/* -------------------------------------------------------------------------- */
593676
/* Helpers; */
594677
/* -------------------------------------------------------------------------- */

0 commit comments

Comments
 (0)