react-ability-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Licence +0 -0
- package/README.md +300 -0
- package/dist/index.cjs +61 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
package/Licence
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
|
|
2
|
+
## Test on the Demo (Local Development)
|
|
3
|
+
|
|
4
|
+
If you want to test the library locally before publishing it to npm, you can link it into a demo React app.
|
|
5
|
+
|
|
6
|
+
### 1. Build the library
|
|
7
|
+
|
|
8
|
+
From the root of the library project:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install
|
|
12
|
+
npm run build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
# React Ability
|
|
16
|
+
|
|
17
|
+
A small, typed permission layer for React that keeps authorization logic **out of your components** and **in one place**.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Core idea (in one sentence)
|
|
22
|
+
|
|
23
|
+
> The package centralizes and standardizes permission logic so your UI doesn’t turn into a mess of `if (user.role === …)` checks scattered everywhere.
|
|
24
|
+
|
|
25
|
+
That’s it. Everything else is implementation details.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## The real problem (what goes wrong in real apps)
|
|
30
|
+
|
|
31
|
+
Let’s start with how apps usually look **without** a permission layer.
|
|
32
|
+
|
|
33
|
+
### ❌ Without a package (today’s reality)
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// Button.tsx
|
|
37
|
+
if (user?.role === "admin") {
|
|
38
|
+
return <DeleteButton />;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// InvoiceRow.tsx
|
|
42
|
+
if (user?.id === invoice.ownerId && invoice.status === "draft") {
|
|
43
|
+
return <EditButton />;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// InvoicePage.tsx
|
|
47
|
+
const canView =
|
|
48
|
+
user &&
|
|
49
|
+
(user.role === "admin" ||
|
|
50
|
+
user.permissions.includes("invoice:read"));
|
|
51
|
+
|
|
52
|
+
// Navbar.tsx
|
|
53
|
+
if (user && user.role !== "guest") {
|
|
54
|
+
showBilling = true;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
Problems this creates
|
|
60
|
+
❌ Logic duplication
|
|
61
|
+
The same rules are written differently in many files.
|
|
62
|
+
|
|
63
|
+
❌ Rules drift
|
|
64
|
+
Someone updates one condition but forgets others.
|
|
65
|
+
|
|
66
|
+
❌ Impossible to audit
|
|
67
|
+
“Who can edit invoices?” → you must search the entire codebase.
|
|
68
|
+
|
|
69
|
+
❌ UI bugs
|
|
70
|
+
Button visible but API rejects
|
|
71
|
+
|
|
72
|
+
Button hidden but API allows
|
|
73
|
+
|
|
74
|
+
❌ No type safety
|
|
75
|
+
ts
|
|
76
|
+
Copier le code
|
|
77
|
+
"inovice:update" // typo = silent bug
|
|
78
|
+
❌ Hard to change roles
|
|
79
|
+
Adding a new role breaks logic everywhere.
|
|
80
|
+
|
|
81
|
+
What this package introduces (the missing abstraction)
|
|
82
|
+
Key idea: policy-first permissions
|
|
83
|
+
Instead of asking:
|
|
84
|
+
|
|
85
|
+
“Can the user do this?”
|
|
86
|
+
|
|
87
|
+
everywhere in the UI…
|
|
88
|
+
|
|
89
|
+
You define rules once, then query them everywhere.
|
|
90
|
+
|
|
91
|
+
Mental model (important)
|
|
92
|
+
Think of your app like this:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
User + Context → Ability → UI decisions
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Your package only handles the Ability part.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
User ──► Policy ──► Ability ──► UI / Components
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
What the package actually solves (concretely)
|
|
105
|
+
1️⃣ Single source of truth for permissions
|
|
106
|
+
Instead of scattered checks, you get one policy file:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// policy.ts
|
|
110
|
+
allow("update", "Invoice", invoice => invoice.ownerId === user.id);
|
|
111
|
+
deny("delete", "Invoice");
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Result:
|
|
115
|
+
|
|
116
|
+
All permission logic lives in one place
|
|
117
|
+
|
|
118
|
+
Easy to review, change, and reason about
|
|
119
|
+
|
|
120
|
+
2️⃣ Turns business rules into readable policies
|
|
121
|
+
❌ Before
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
if (
|
|
125
|
+
user &&
|
|
126
|
+
user.role !== "guest" &&
|
|
127
|
+
invoice.ownerId === user.id &&
|
|
128
|
+
invoice.status === "draft"
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
✅ With the package
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
allow(
|
|
136
|
+
"update",
|
|
137
|
+
"Invoice",
|
|
138
|
+
i => i.ownerId === user.id && i.status === "draft"
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This is domain language, not UI logic.
|
|
143
|
+
|
|
144
|
+
3️⃣ Removes permission logic from components
|
|
145
|
+
❌ Before
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
{user?.role === "admin" && <DeleteButton />}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
✅ After
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
<Can I="delete" a="Invoice">
|
|
155
|
+
<DeleteButton />
|
|
156
|
+
</Can>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Components now care only about UI, not authorization details.
|
|
160
|
+
|
|
161
|
+
4️⃣ Prevents permission bugs at compile time (TypeScript win)
|
|
162
|
+
This is huge.
|
|
163
|
+
|
|
164
|
+
❌ Without typing
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
can("updtae", "Invioce"); // typo, no error
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
✅ With this package
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
can("updtae", "Invioce");
|
|
174
|
+
// ❌ TypeScript error immediately
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This eliminates an entire class of bugs.
|
|
178
|
+
|
|
179
|
+
5️⃣ Makes ownership rules first-class (not hacks)
|
|
180
|
+
Ownership checks are usually scattered:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
if (invoice.ownerId === user.id)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
With this package:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
allow("update", "Invoice", invoice => invoice.ownerId === user.id);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Ownership logic becomes:
|
|
193
|
+
|
|
194
|
+
consistent
|
|
195
|
+
|
|
196
|
+
reusable
|
|
197
|
+
|
|
198
|
+
testable
|
|
199
|
+
|
|
200
|
+
6️⃣ Makes SSR and hydration predictable
|
|
201
|
+
Without a system:
|
|
202
|
+
|
|
203
|
+
UI flickers
|
|
204
|
+
|
|
205
|
+
Buttons appear/disappear after hydration
|
|
206
|
+
|
|
207
|
+
Different logic runs on server vs client
|
|
208
|
+
|
|
209
|
+
With this package:
|
|
210
|
+
|
|
211
|
+
Ability is created once from the same user data
|
|
212
|
+
|
|
213
|
+
Server and client render the same decisions
|
|
214
|
+
|
|
215
|
+
What the <Can /> component really is
|
|
216
|
+
It’s not magic.
|
|
217
|
+
|
|
218
|
+
It simply means:
|
|
219
|
+
|
|
220
|
+
“Render children only if a permission rule passes.”
|
|
221
|
+
|
|
222
|
+
Instead of:
|
|
223
|
+
```tsx
|
|
224
|
+
if (!canEdit) return null;
|
|
225
|
+
```
|
|
226
|
+
You write:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
<Can I="update" a="Invoice" this={invoice}>
|
|
230
|
+
<EditButton />
|
|
231
|
+
</Can>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
That’s it.
|
|
235
|
+
|
|
236
|
+
What this package is NOT
|
|
237
|
+
This is important.
|
|
238
|
+
|
|
239
|
+
❌ Not an auth system
|
|
240
|
+
❌ Not a backend security layer
|
|
241
|
+
❌ Not a role manager UI
|
|
242
|
+
❌ Not a permission database
|
|
243
|
+
|
|
244
|
+
This package:
|
|
245
|
+
|
|
246
|
+
does not replace backend checks
|
|
247
|
+
|
|
248
|
+
does not handle authentication
|
|
249
|
+
|
|
250
|
+
does not store roles
|
|
251
|
+
|
|
252
|
+
It only answers one question:
|
|
253
|
+
|
|
254
|
+
“Given a user and a resource, is this action allowed?”
|
|
255
|
+
|
|
256
|
+
When this package makes sense
|
|
257
|
+
✅ SaaS dashboards
|
|
258
|
+
✅ Multi-role apps
|
|
259
|
+
✅ B2B products
|
|
260
|
+
✅ Apps with ownership rules
|
|
261
|
+
✅ Teams larger than 1 developer
|
|
262
|
+
|
|
263
|
+
When it’s overkill
|
|
264
|
+
❌ Landing pages
|
|
265
|
+
❌ Simple blogs
|
|
266
|
+
❌ Apps with only admin / non-admin logic
|
|
267
|
+
|
|
268
|
+
Why this is worth publishing
|
|
269
|
+
Most developers:
|
|
270
|
+
|
|
271
|
+
feel this pain
|
|
272
|
+
|
|
273
|
+
write ad-hoc permission logic
|
|
274
|
+
|
|
275
|
+
never extract it cleanly
|
|
276
|
+
|
|
277
|
+
This package:
|
|
278
|
+
|
|
279
|
+
gives a clear, repeatable pattern
|
|
280
|
+
|
|
281
|
+
provides excellent TypeScript DX
|
|
282
|
+
|
|
283
|
+
keeps the API small and focused
|
|
284
|
+
|
|
285
|
+
That’s exactly what successful small libraries do.
|
|
286
|
+
|
|
287
|
+
Final simplified summary
|
|
288
|
+
This package solves one problem:
|
|
289
|
+
|
|
290
|
+
“How do I express and use permissions in React without scattering fragile conditional logic everywhere?”
|
|
291
|
+
|
|
292
|
+
It solves it by:
|
|
293
|
+
|
|
294
|
+
centralizing permission rules
|
|
295
|
+
|
|
296
|
+
typing actions and resources
|
|
297
|
+
|
|
298
|
+
exposing a clean can() API
|
|
299
|
+
|
|
300
|
+
providing <Can /> for UI rendering
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/factory.ts
|
|
7
|
+
function createAbilityKit() {
|
|
8
|
+
function createAbility(rules) {
|
|
9
|
+
const compiled = rules.slice();
|
|
10
|
+
const can = (action, subject, obj) => {
|
|
11
|
+
let verdict;
|
|
12
|
+
for (const rule of compiled) {
|
|
13
|
+
if (rule.subject !== subject) continue;
|
|
14
|
+
if (rule.action !== action) continue;
|
|
15
|
+
if (rule.when) {
|
|
16
|
+
if (!obj) continue;
|
|
17
|
+
if (!rule.when(obj)) continue;
|
|
18
|
+
}
|
|
19
|
+
verdict = rule.inverted ? false : true;
|
|
20
|
+
}
|
|
21
|
+
return verdict ?? false;
|
|
22
|
+
};
|
|
23
|
+
return { can };
|
|
24
|
+
}
|
|
25
|
+
function defineRules(builder) {
|
|
26
|
+
const rules = [];
|
|
27
|
+
const allow = (action, subject, when) => {
|
|
28
|
+
rules.push({ action, subject, when });
|
|
29
|
+
};
|
|
30
|
+
const deny = (action, subject, when) => {
|
|
31
|
+
rules.push({ action, subject, when, inverted: true });
|
|
32
|
+
};
|
|
33
|
+
builder(allow, deny);
|
|
34
|
+
return rules;
|
|
35
|
+
}
|
|
36
|
+
return { createAbility, defineRules };
|
|
37
|
+
}
|
|
38
|
+
function createReactAbilityKit() {
|
|
39
|
+
const Ctx = react.createContext(null);
|
|
40
|
+
function AbilityProvider({
|
|
41
|
+
ability,
|
|
42
|
+
children
|
|
43
|
+
}) {
|
|
44
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Ctx.Provider, { value: ability, children });
|
|
45
|
+
}
|
|
46
|
+
function useAbility() {
|
|
47
|
+
const ability = react.useContext(Ctx);
|
|
48
|
+
if (!ability) throw new Error("useAbility must be used within AbilityProvider");
|
|
49
|
+
return ability;
|
|
50
|
+
}
|
|
51
|
+
function Can(props) {
|
|
52
|
+
const { can } = useAbility();
|
|
53
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: can(props.I, props.a, props.this) ? props.children : props.fallback ?? null });
|
|
54
|
+
}
|
|
55
|
+
return { AbilityProvider, useAbility, Can };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
exports.createAbilityKit = createAbilityKit;
|
|
59
|
+
exports.createReactAbilityKit = createReactAbilityKit;
|
|
60
|
+
//# sourceMappingURL=index.cjs.map
|
|
61
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/factory.ts","../src/react-typed.tsx"],"names":["createContext","useContext","jsx","Fragment"],"mappings":";;;;;;AAoBO,SAAS,gBAAA,GAAwD;AAItE,EAAA,SAAS,cAAc,KAAA,EAAiD;AACtE,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,EAAM;AAE7B,IAAA,MAAM,GAAA,GAAM,CACV,MAAA,EACA,OAAA,EACA,GAAA,KACG;AACH,MAAA,IAAI,OAAA;AAEJ,MAAA,KAAA,MAAW,QAAQ,QAAA,EAAU;AAC3B,QAAA,IAAI,IAAA,CAAK,YAAY,OAAA,EAAS;AAC9B,QAAA,IAAI,IAAA,CAAK,WAAW,MAAA,EAAQ;AAE5B,QAAA,IAAI,KAAK,IAAA,EAAM;AACb,UAAA,IAAI,CAAC,GAAA,EAAK;AACV,UAAA,IAAI,CAAC,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA,EAAG;AAAA,QACvB;AAEA,QAAA,OAAA,GAAU,IAAA,CAAK,WAAW,KAAA,GAAQ,IAAA;AAAA,MACpC;AAEA,MAAA,OAAO,OAAA,IAAW,KAAA;AAAA,IACpB,CAAA;AAEA,IAAA,OAAO,EAAE,GAAA,EAAI;AAAA,EACf;AAcA,EAAA,SAAS,YAAY,OAAA,EAAiD;AACpE,IAAA,MAAM,QAAmB,EAAC;AAC1B,IAAA,MAAM,KAAA,GAAiB,CAAC,MAAA,EAAQ,OAAA,EAAS,IAAA,KAAS;AAChD,MAAA,KAAA,CAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,OAAA,EAAS,MAAiB,CAAA;AAAA,IACjD,CAAA;AACA,IAAA,MAAM,IAAA,GAAe,CAAC,MAAA,EAAQ,OAAA,EAAS,IAAA,KAAS;AAC9C,MAAA,KAAA,CAAM,KAAK,EAAE,MAAA,EAAQ,SAAS,IAAA,EAAM,QAAA,EAAU,MAAiB,CAAA;AAAA,IACjE,CAAA;AACA,IAAA,OAAA,CAAQ,OAAO,IAAI,CAAA;AACnB,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,EAAE,eAAe,WAAA,EAAY;AACtC;ACzEO,SAAS,qBAAA,GAA6D;AACzE,EAAA,MAAM,GAAA,GAAMA,oBAAoD,IAAI,CAAA;AAEpE,EAAA,SAAS,eAAA,CAAgB;AAAA,IACrB,OAAA;AAAA,IACA;AAAA,GACJ,EAGG;AACC,IAAA,sCAAQ,GAAA,CAAI,QAAA,EAAJ,EAAa,KAAA,EAAO,SAAU,QAAA,EAAS,CAAA;AAAA,EACnD;AAEA,EAAA,SAAS,UAAA,GAAa;AAClB,IAAA,MAAM,OAAA,GAAUC,iBAAW,GAAG,CAAA;AAC9B,IAAA,IAAI,CAAC,OAAA,EAAS,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAC9E,IAAA,OAAO,OAAA;AAAA,EACX;AAEA,EAAA,SAAS,IAAiC,KAAA,EAMvC;AACC,IAAA,MAAM,EAAE,GAAA,EAAI,GAAI,UAAA,EAAW;AAC3B,IAAA,uBAAOC,cAAA,CAAAC,mBAAA,EAAA,EAAG,QAAA,EAAA,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,IAAI,CAAA,GAAI,KAAA,CAAM,QAAA,GAAW,KAAA,CAAM,YAAY,IAAA,EAAK,CAAA;AAAA,EAC1F;AAEA,EAAA,OAAO,EAAE,eAAA,EAAiB,UAAA,EAAY,GAAA,EAAI;AAC9C","file":"index.cjs","sourcesContent":["// src/factory.ts\r\nexport type Condition<SubjectsMap, S extends keyof SubjectsMap> = (\r\n obj: SubjectsMap[S]\r\n) => boolean;\r\n\r\nexport type Rule<Actions extends string, SubjectsMap, S extends keyof SubjectsMap = keyof SubjectsMap> = {\r\n action: Actions;\r\n subject: S;\r\n when?: Condition<SubjectsMap, S>;\r\n inverted?: boolean;\r\n};\r\n\r\nexport type Ability<Actions extends string, SubjectsMap> = {\r\n can: <S extends keyof SubjectsMap>(\r\n action: Actions,\r\n subject: S,\r\n obj?: SubjectsMap[S]\r\n ) => boolean;\r\n};\r\n\r\nexport function createAbilityKit<Actions extends string, SubjectsMap>() {\r\n type Subject = keyof SubjectsMap;\r\n type AnyRule = Rule<Actions, SubjectsMap, any>;\r\n\r\n function createAbility(rules: AnyRule[]): Ability<Actions, SubjectsMap> {\r\n const compiled = rules.slice();\r\n\r\n const can = <S extends Subject>(\r\n action: Actions,\r\n subject: S,\r\n obj?: SubjectsMap[S]\r\n ) => {\r\n let verdict: boolean | undefined;\r\n\r\n for (const rule of compiled) {\r\n if (rule.subject !== subject) continue;\r\n if (rule.action !== action) continue;\r\n\r\n if (rule.when) {\r\n if (!obj) continue;\r\n if (!rule.when(obj)) continue;\r\n }\r\n\r\n verdict = rule.inverted ? false : true;\r\n }\r\n\r\n return verdict ?? false;\r\n };\r\n\r\n return { can };\r\n }\r\n\r\n type AllowFn = <S extends Subject>(\r\n action: Actions,\r\n subject: S,\r\n when?: Condition<SubjectsMap, S>\r\n ) => void;\r\n\r\n type DenyFn = <S extends Subject>(\r\n action: Actions,\r\n subject: S,\r\n when?: Condition<SubjectsMap, S>\r\n ) => void;\r\n\r\n function defineRules(builder: (allow: AllowFn, deny: DenyFn) => void) {\r\n const rules: AnyRule[] = [];\r\n const allow: AllowFn = (action, subject, when) => {\r\n rules.push({ action, subject, when } as AnyRule);\r\n };\r\n const deny: DenyFn = (action, subject, when) => {\r\n rules.push({ action, subject, when, inverted: true } as AnyRule);\r\n };\r\n builder(allow, deny);\r\n return rules;\r\n }\r\n\r\n return { createAbility, defineRules };\r\n}\r\n","// src/react-typed.tsx\r\nimport React, { createContext, useContext } from \"react\";\r\nimport type { Ability } from \"./factory\";\r\n\r\nexport function createReactAbilityKit<Actions extends string, SubjectsMap>() {\r\n const Ctx = createContext<Ability<Actions, SubjectsMap> | null>(null);\r\n\r\n function AbilityProvider({\r\n ability,\r\n children,\r\n }: {\r\n ability: Ability<Actions, SubjectsMap>;\r\n children: React.ReactNode;\r\n }) {\r\n return <Ctx.Provider value={ability}>{children}</Ctx.Provider>;\r\n }\r\n\r\n function useAbility() {\r\n const ability = useContext(Ctx);\r\n if (!ability) throw new Error(\"useAbility must be used within AbilityProvider\");\r\n return ability;\r\n }\r\n\r\n function Can<S extends keyof SubjectsMap>(props: {\r\n I: Actions;\r\n a: S;\r\n this?: SubjectsMap[S];\r\n fallback?: React.ReactNode;\r\n children: React.ReactNode;\r\n }) {\r\n const { can } = useAbility();\r\n return <>{can(props.I, props.a, props.this) ? props.children : props.fallback ?? null}</>;\r\n }\r\n\r\n return { AbilityProvider, useAbility, Can };\r\n}\r\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
type Condition<SubjectsMap, S extends keyof SubjectsMap> = (obj: SubjectsMap[S]) => boolean;
|
|
5
|
+
type Rule<Actions extends string, SubjectsMap, S extends keyof SubjectsMap = keyof SubjectsMap> = {
|
|
6
|
+
action: Actions;
|
|
7
|
+
subject: S;
|
|
8
|
+
when?: Condition<SubjectsMap, S>;
|
|
9
|
+
inverted?: boolean;
|
|
10
|
+
};
|
|
11
|
+
type Ability<Actions extends string, SubjectsMap> = {
|
|
12
|
+
can: <S extends keyof SubjectsMap>(action: Actions, subject: S, obj?: SubjectsMap[S]) => boolean;
|
|
13
|
+
};
|
|
14
|
+
declare function createAbilityKit<Actions extends string, SubjectsMap>(): {
|
|
15
|
+
createAbility: (rules: Rule<Actions, SubjectsMap, any>[]) => Ability<Actions, SubjectsMap>;
|
|
16
|
+
defineRules: (builder: (allow: <S extends keyof SubjectsMap>(action: Actions, subject: S, when?: Condition<SubjectsMap, S>) => void, deny: <S extends keyof SubjectsMap>(action: Actions, subject: S, when?: Condition<SubjectsMap, S>) => void) => void) => Rule<Actions, SubjectsMap, any>[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
declare function createReactAbilityKit<Actions extends string, SubjectsMap>(): {
|
|
20
|
+
AbilityProvider: ({ ability, children, }: {
|
|
21
|
+
ability: Ability<Actions, SubjectsMap>;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
24
|
+
useAbility: () => Ability<Actions, SubjectsMap>;
|
|
25
|
+
Can: <S extends keyof SubjectsMap>(props: {
|
|
26
|
+
I: Actions;
|
|
27
|
+
a: S;
|
|
28
|
+
this?: SubjectsMap[S];
|
|
29
|
+
fallback?: React.ReactNode;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { type Ability, type Condition, type Rule, createAbilityKit, createReactAbilityKit };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
type Condition<SubjectsMap, S extends keyof SubjectsMap> = (obj: SubjectsMap[S]) => boolean;
|
|
5
|
+
type Rule<Actions extends string, SubjectsMap, S extends keyof SubjectsMap = keyof SubjectsMap> = {
|
|
6
|
+
action: Actions;
|
|
7
|
+
subject: S;
|
|
8
|
+
when?: Condition<SubjectsMap, S>;
|
|
9
|
+
inverted?: boolean;
|
|
10
|
+
};
|
|
11
|
+
type Ability<Actions extends string, SubjectsMap> = {
|
|
12
|
+
can: <S extends keyof SubjectsMap>(action: Actions, subject: S, obj?: SubjectsMap[S]) => boolean;
|
|
13
|
+
};
|
|
14
|
+
declare function createAbilityKit<Actions extends string, SubjectsMap>(): {
|
|
15
|
+
createAbility: (rules: Rule<Actions, SubjectsMap, any>[]) => Ability<Actions, SubjectsMap>;
|
|
16
|
+
defineRules: (builder: (allow: <S extends keyof SubjectsMap>(action: Actions, subject: S, when?: Condition<SubjectsMap, S>) => void, deny: <S extends keyof SubjectsMap>(action: Actions, subject: S, when?: Condition<SubjectsMap, S>) => void) => void) => Rule<Actions, SubjectsMap, any>[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
declare function createReactAbilityKit<Actions extends string, SubjectsMap>(): {
|
|
20
|
+
AbilityProvider: ({ ability, children, }: {
|
|
21
|
+
ability: Ability<Actions, SubjectsMap>;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
24
|
+
useAbility: () => Ability<Actions, SubjectsMap>;
|
|
25
|
+
Can: <S extends keyof SubjectsMap>(props: {
|
|
26
|
+
I: Actions;
|
|
27
|
+
a: S;
|
|
28
|
+
this?: SubjectsMap[S];
|
|
29
|
+
fallback?: React.ReactNode;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { type Ability, type Condition, type Rule, createAbilityKit, createReactAbilityKit };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/factory.ts
|
|
5
|
+
function createAbilityKit() {
|
|
6
|
+
function createAbility(rules) {
|
|
7
|
+
const compiled = rules.slice();
|
|
8
|
+
const can = (action, subject, obj) => {
|
|
9
|
+
let verdict;
|
|
10
|
+
for (const rule of compiled) {
|
|
11
|
+
if (rule.subject !== subject) continue;
|
|
12
|
+
if (rule.action !== action) continue;
|
|
13
|
+
if (rule.when) {
|
|
14
|
+
if (!obj) continue;
|
|
15
|
+
if (!rule.when(obj)) continue;
|
|
16
|
+
}
|
|
17
|
+
verdict = rule.inverted ? false : true;
|
|
18
|
+
}
|
|
19
|
+
return verdict ?? false;
|
|
20
|
+
};
|
|
21
|
+
return { can };
|
|
22
|
+
}
|
|
23
|
+
function defineRules(builder) {
|
|
24
|
+
const rules = [];
|
|
25
|
+
const allow = (action, subject, when) => {
|
|
26
|
+
rules.push({ action, subject, when });
|
|
27
|
+
};
|
|
28
|
+
const deny = (action, subject, when) => {
|
|
29
|
+
rules.push({ action, subject, when, inverted: true });
|
|
30
|
+
};
|
|
31
|
+
builder(allow, deny);
|
|
32
|
+
return rules;
|
|
33
|
+
}
|
|
34
|
+
return { createAbility, defineRules };
|
|
35
|
+
}
|
|
36
|
+
function createReactAbilityKit() {
|
|
37
|
+
const Ctx = createContext(null);
|
|
38
|
+
function AbilityProvider({
|
|
39
|
+
ability,
|
|
40
|
+
children
|
|
41
|
+
}) {
|
|
42
|
+
return /* @__PURE__ */ jsx(Ctx.Provider, { value: ability, children });
|
|
43
|
+
}
|
|
44
|
+
function useAbility() {
|
|
45
|
+
const ability = useContext(Ctx);
|
|
46
|
+
if (!ability) throw new Error("useAbility must be used within AbilityProvider");
|
|
47
|
+
return ability;
|
|
48
|
+
}
|
|
49
|
+
function Can(props) {
|
|
50
|
+
const { can } = useAbility();
|
|
51
|
+
return /* @__PURE__ */ jsx(Fragment, { children: can(props.I, props.a, props.this) ? props.children : props.fallback ?? null });
|
|
52
|
+
}
|
|
53
|
+
return { AbilityProvider, useAbility, Can };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { createAbilityKit, createReactAbilityKit };
|
|
57
|
+
//# sourceMappingURL=index.js.map
|
|
58
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/factory.ts","../src/react-typed.tsx"],"names":[],"mappings":";;;;AAoBO,SAAS,gBAAA,GAAwD;AAItE,EAAA,SAAS,cAAc,KAAA,EAAiD;AACtE,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,EAAM;AAE7B,IAAA,MAAM,GAAA,GAAM,CACV,MAAA,EACA,OAAA,EACA,GAAA,KACG;AACH,MAAA,IAAI,OAAA;AAEJ,MAAA,KAAA,MAAW,QAAQ,QAAA,EAAU;AAC3B,QAAA,IAAI,IAAA,CAAK,YAAY,OAAA,EAAS;AAC9B,QAAA,IAAI,IAAA,CAAK,WAAW,MAAA,EAAQ;AAE5B,QAAA,IAAI,KAAK,IAAA,EAAM;AACb,UAAA,IAAI,CAAC,GAAA,EAAK;AACV,UAAA,IAAI,CAAC,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA,EAAG;AAAA,QACvB;AAEA,QAAA,OAAA,GAAU,IAAA,CAAK,WAAW,KAAA,GAAQ,IAAA;AAAA,MACpC;AAEA,MAAA,OAAO,OAAA,IAAW,KAAA;AAAA,IACpB,CAAA;AAEA,IAAA,OAAO,EAAE,GAAA,EAAI;AAAA,EACf;AAcA,EAAA,SAAS,YAAY,OAAA,EAAiD;AACpE,IAAA,MAAM,QAAmB,EAAC;AAC1B,IAAA,MAAM,KAAA,GAAiB,CAAC,MAAA,EAAQ,OAAA,EAAS,IAAA,KAAS;AAChD,MAAA,KAAA,CAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,OAAA,EAAS,MAAiB,CAAA;AAAA,IACjD,CAAA;AACA,IAAA,MAAM,IAAA,GAAe,CAAC,MAAA,EAAQ,OAAA,EAAS,IAAA,KAAS;AAC9C,MAAA,KAAA,CAAM,KAAK,EAAE,MAAA,EAAQ,SAAS,IAAA,EAAM,QAAA,EAAU,MAAiB,CAAA;AAAA,IACjE,CAAA;AACA,IAAA,OAAA,CAAQ,OAAO,IAAI,CAAA;AACnB,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,EAAE,eAAe,WAAA,EAAY;AACtC;ACzEO,SAAS,qBAAA,GAA6D;AACzE,EAAA,MAAM,GAAA,GAAM,cAAoD,IAAI,CAAA;AAEpE,EAAA,SAAS,eAAA,CAAgB;AAAA,IACrB,OAAA;AAAA,IACA;AAAA,GACJ,EAGG;AACC,IAAA,2BAAQ,GAAA,CAAI,QAAA,EAAJ,EAAa,KAAA,EAAO,SAAU,QAAA,EAAS,CAAA;AAAA,EACnD;AAEA,EAAA,SAAS,UAAA,GAAa;AAClB,IAAA,MAAM,OAAA,GAAU,WAAW,GAAG,CAAA;AAC9B,IAAA,IAAI,CAAC,OAAA,EAAS,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAC9E,IAAA,OAAO,OAAA;AAAA,EACX;AAEA,EAAA,SAAS,IAAiC,KAAA,EAMvC;AACC,IAAA,MAAM,EAAE,GAAA,EAAI,GAAI,UAAA,EAAW;AAC3B,IAAA,uBAAO,GAAA,CAAA,QAAA,EAAA,EAAG,QAAA,EAAA,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,IAAI,CAAA,GAAI,KAAA,CAAM,QAAA,GAAW,KAAA,CAAM,YAAY,IAAA,EAAK,CAAA;AAAA,EAC1F;AAEA,EAAA,OAAO,EAAE,eAAA,EAAiB,UAAA,EAAY,GAAA,EAAI;AAC9C","file":"index.js","sourcesContent":["// src/factory.ts\r\nexport type Condition<SubjectsMap, S extends keyof SubjectsMap> = (\r\n obj: SubjectsMap[S]\r\n) => boolean;\r\n\r\nexport type Rule<Actions extends string, SubjectsMap, S extends keyof SubjectsMap = keyof SubjectsMap> = {\r\n action: Actions;\r\n subject: S;\r\n when?: Condition<SubjectsMap, S>;\r\n inverted?: boolean;\r\n};\r\n\r\nexport type Ability<Actions extends string, SubjectsMap> = {\r\n can: <S extends keyof SubjectsMap>(\r\n action: Actions,\r\n subject: S,\r\n obj?: SubjectsMap[S]\r\n ) => boolean;\r\n};\r\n\r\nexport function createAbilityKit<Actions extends string, SubjectsMap>() {\r\n type Subject = keyof SubjectsMap;\r\n type AnyRule = Rule<Actions, SubjectsMap, any>;\r\n\r\n function createAbility(rules: AnyRule[]): Ability<Actions, SubjectsMap> {\r\n const compiled = rules.slice();\r\n\r\n const can = <S extends Subject>(\r\n action: Actions,\r\n subject: S,\r\n obj?: SubjectsMap[S]\r\n ) => {\r\n let verdict: boolean | undefined;\r\n\r\n for (const rule of compiled) {\r\n if (rule.subject !== subject) continue;\r\n if (rule.action !== action) continue;\r\n\r\n if (rule.when) {\r\n if (!obj) continue;\r\n if (!rule.when(obj)) continue;\r\n }\r\n\r\n verdict = rule.inverted ? false : true;\r\n }\r\n\r\n return verdict ?? false;\r\n };\r\n\r\n return { can };\r\n }\r\n\r\n type AllowFn = <S extends Subject>(\r\n action: Actions,\r\n subject: S,\r\n when?: Condition<SubjectsMap, S>\r\n ) => void;\r\n\r\n type DenyFn = <S extends Subject>(\r\n action: Actions,\r\n subject: S,\r\n when?: Condition<SubjectsMap, S>\r\n ) => void;\r\n\r\n function defineRules(builder: (allow: AllowFn, deny: DenyFn) => void) {\r\n const rules: AnyRule[] = [];\r\n const allow: AllowFn = (action, subject, when) => {\r\n rules.push({ action, subject, when } as AnyRule);\r\n };\r\n const deny: DenyFn = (action, subject, when) => {\r\n rules.push({ action, subject, when, inverted: true } as AnyRule);\r\n };\r\n builder(allow, deny);\r\n return rules;\r\n }\r\n\r\n return { createAbility, defineRules };\r\n}\r\n","// src/react-typed.tsx\r\nimport React, { createContext, useContext } from \"react\";\r\nimport type { Ability } from \"./factory\";\r\n\r\nexport function createReactAbilityKit<Actions extends string, SubjectsMap>() {\r\n const Ctx = createContext<Ability<Actions, SubjectsMap> | null>(null);\r\n\r\n function AbilityProvider({\r\n ability,\r\n children,\r\n }: {\r\n ability: Ability<Actions, SubjectsMap>;\r\n children: React.ReactNode;\r\n }) {\r\n return <Ctx.Provider value={ability}>{children}</Ctx.Provider>;\r\n }\r\n\r\n function useAbility() {\r\n const ability = useContext(Ctx);\r\n if (!ability) throw new Error(\"useAbility must be used within AbilityProvider\");\r\n return ability;\r\n }\r\n\r\n function Can<S extends keyof SubjectsMap>(props: {\r\n I: Actions;\r\n a: S;\r\n this?: SubjectsMap[S];\r\n fallback?: React.ReactNode;\r\n children: React.ReactNode;\r\n }) {\r\n const { can } = useAbility();\r\n return <>{can(props.I, props.a, props.this) ? props.children : props.fallback ?? null}</>;\r\n }\r\n\r\n return { AbilityProvider, useAbility, Can };\r\n}\r\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-ability-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.0.0",
|
|
25
|
+
"@types/react": "^18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
}
|
|
33
|
+
}
|