diff --git a/README.md b/README.md index 8c2539d..a693ce9 100644 --- a/README.md +++ b/README.md @@ -399,18 +399,41 @@ Evaluates all the policies with `check` and returns a snapshot with the results. It's useful to serialize policies. +It takes an array of policies. If a policy does not take an argument, it can be passed as is. Policies that take an argument have to be passed as a tuple with the argument. + ```ts -export function checkAllSettle( - policies: TPolicies -): PoliciesSnapshot +type PolicyTuple = + | Policy + | readonly [string, PolicyConditionNoArg] + | readonly [Policy] + | readonly [Policy, any]; +type InferPolicyName = TPolicyTuple extends readonly [infer name, any] + ? name extends Policy + ? Name + : name extends string + ? name + : never + : TPolicyTuple extends readonly [Policy] + ? Name + : TPolicyTuple extends Policy + ? Name + : never; +type PoliciesSnapshot = { [K in TPolicyName]: boolean }; + +export function checkAllSettle< + const TPolicies extends readonly PolicyTuple[], + TPolicyTuple extends TPolicies[number], + TPolicyName extends InferPolicyName, +>(policies: TPolicies): PoliciesSnapshot ``` Example: ```ts // TLDR const snapshot = checkAllSettle([ - [guard.post.policy("is my post"), post], - ["post has comments", post.comments.length > 0], + [guard.post.policy("is my post"), post], // Policy with argument + ["post has comments", post.comments.length > 0], // Implicit policy with no argument + definePolicy("post has likes", post.likes.length > 0), // Policy without argument ]); // Example @@ -436,9 +459,10 @@ const guard = { const snapshot = checkAllSettle([ [guard.post.policy("is my post"), post], ["post has comments", post.comments.length > 0], + definePolicy("post has likes", post.likes.length > 0), ]); -console.log(snapshot); // { "is my post": boolean; "post has comments": boolean; } +console.log(snapshot); // { "is my post": boolean; "post has comments": boolean; "post has likes": boolean } console.log(snapshot["is my post"]) // boolean ``` diff --git a/package-lock.json b/package-lock.json index a049e0b..ddf6764 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "comply", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "comply", - "version": "0.3.1", + "version": "0.4.0", "license": "MIT", "workspaces": [ ".", diff --git a/package.json b/package.json index cbc8a0f..43c7300 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "comply", - "version": "0.3.1", + "version": "0.4.0", "description": "Comply is a tiny library to help you define policies in your app", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/src/index.test.ts b/src/index.test.ts index e148def..23a88bd 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -895,14 +895,14 @@ describe("Check all settle", () => { it("should snapshot policies", () => { const PostPolicies = definePolicies((context: Context) => { const myPostPolicy = definePolicy( - "my post", + "shared policy within policy set", (post: Post) => post.userId === context.userId, () => new Error("Not the author") ); return [ myPostPolicy, - definePolicy("all my published posts", (post: Post) => + definePolicy("policy from policy set", (post: Post) => and(check(myPostPolicy, post), post.status === "published") ), ]; @@ -913,36 +913,63 @@ describe("Check all settle", () => { }; 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" }], + [definePolicy("policy with arg", notNull), "not null"], + ["implicit policy with arg", notNull, "not null"], + [definePolicy("policy with no arg", true)], + definePolicy("policy with no arg simple version", true), + ["implicit policy with boolean", true], + ["implicit policy with no arg function", () => true], + [guard.post.policy("shared policy within policy set"), { userId: "1", comments: [], status: "published" }], + [guard.post.policy("policy from policy set"), { 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, + "policy with arg": true, + "implicit policy with arg": true, + "policy with no arg": true, + "policy with no arg simple version": true, + "implicit policy with boolean": true, + "implicit policy with no arg function": true, + "shared policy within policy set": true, + "policy from policy set": 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; + "policy with arg": boolean; + "implicit policy with arg": boolean; + "policy with no arg": boolean; + "policy with no arg simple version": boolean; + "implicit policy with boolean": boolean; + "implicit policy with no arg function": boolean; + "shared policy within policy set": boolean; + "policy from policy set": 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<{ + expectTypeOf( + checkAllSettle([ + [ + /** @ts-expect-error */ + definePolicy("is not null", notNull), + ], + ]) + ).toEqualTypeOf<{ [x: string]: boolean }>(); + expectTypeOf( + checkAllSettle([ + [ + /** @ts-expect-error */ + definePolicy("is true", true), + "extra arg", + ], + ]) + ).toEqualTypeOf<{ + [x: string]: boolean; + }>(); + expectTypeOf( + checkAllSettle([ + /** @ts-expect-error */ + definePolicy("is true", (v: unknown) => true), + ]) + ).toEqualTypeOf<{ [x: string]: boolean; }>(); }); diff --git a/src/index.ts b/src/index.ts index aefde32..d5b3c48 100644 --- a/src/index.ts +++ b/src/index.ts @@ -590,19 +590,23 @@ export function check( } type PolicyTuple = + | Policy | readonly [string, PolicyConditionNoArg] + | readonly [string, PolicyConditionWithArg, any] | readonly [Policy] | readonly [Policy, any]; -type InferPolicyName = TPolicyTuple extends readonly [infer name, any] - ? name extends Policy +type InferPolicyName = TPolicyTuple extends readonly [infer NameOrPolicy, ...any[]] + ? NameOrPolicy extends Policy ? Name - : name extends string - ? name + : NameOrPolicy extends string + ? NameOrPolicy : never : TPolicyTuple extends readonly [Policy] ? Name - : never; + : TPolicyTuple extends Policy + ? Name + : never; type PoliciesSnapshot = { [K in TPolicyName]: boolean }; @@ -611,14 +615,17 @@ type PoliciesSnapshot = { [K in TPolicyName]: boolea * * It evaluates all the policies with `check` * + * If a policy does not take an argument, it can be passed as is. Policies that take an argument have to be passed as a tuple with the argument. + * * @param policies - A tuple of policies and their arguments (if needed) * * @example * ```ts * // TLDR const snapshot = checkAllSettle([ - [guard.post.policy("is my post"), post], - ["post has comments", post.comments.length > 0], + [guard.post.policy("is my post"), post], // Policy with argument + ["post has comments", post.comments.length > 0], // Implicit policy with no argument + definePolicy("post has likes", post.likes.length > 0), // Policy without argument. Can be used as is ]); // Example @@ -642,11 +649,12 @@ type PoliciesSnapshot = { [K in TPolicyName]: boolea }; const snapshot = checkAllSettle([ - [guard.post.policy("is my post"), post], - ["post has comments", post.comments.length > 0], + [guard.post.policy("is my post"), post], // A policy with an argument + ["post has comments", post.comments.length > 0], // An implicit policy with no argument + definePolicy("post has likes", post.likes.length > 0), // A policy without argument. Can be used as is ]); - console.log(snapshot); // { "is my post": boolean; "post has comments": boolean; } + console.log(snapshot); // { "is my post": boolean; "post has comments": boolean; "post has likes": boolean } console.log(snapshot["is my post"]) // boolean * ``` */ @@ -656,12 +664,27 @@ export function checkAllSettle< TPolicyName extends InferPolicyName, >(policies: TPolicies): PoliciesSnapshot { 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); + (acc, policyOrTuple) => { + let policyName: string; + let result: boolean; + + if (policyOrTuple instanceof Policy) { + // Policy without argument + policyName = policyOrTuple.name; + result = policyOrTuple.check(); + } else { + // Policy with argument + const [policyOrName, conditionOrArg, implicitPolicyArg] = policyOrTuple; + policyName = typeof policyOrName === "string" ? policyOrName : policyOrName.name; + result = + typeof policyOrName === "string" + ? typeof conditionOrArg === "function" + ? conditionOrArg(implicitPolicyArg) + : conditionOrArg + : policyOrName.check(conditionOrArg); + } + + acc[policyName as TPolicyName] = result; return acc; },