@prsm/entitle 2.0.0 → 2.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/README.md +3 -2
- package/package.json +1 -1
- package/src/entitlements.js +3 -19
- package/src/memoryDriver.js +16 -2
- package/src/postgresDriver.js +23 -4
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ const entitlements = createEntitlements({
|
|
|
91
91
|
meter,
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
+
// account.id is assigned the "pro" plan, whose tokens limit is 5_000_000
|
|
94
95
|
const quota = await entitlements.check(account.id, "tokens")
|
|
95
96
|
// { allowed: true, used: 84210, remaining: 4915790, limit: 5000000, unit: "tokens", feature: "tokens" }
|
|
96
97
|
|
|
@@ -117,7 +118,7 @@ The **plan catalog** is declared once at construction, the way you declare your
|
|
|
117
118
|
|
|
118
119
|
**Assignments** (which plan a subject is on) and **overrides** (per-subject adjustments) live in postgres and are mutable at runtime. An override shallow-merges over the plan, so you specify only what differs. A subject with no assignment gets `defaultPlan`; `unassign(subject)` removes an assignment and reverts the subject to the default (distinct from assigning the default explicitly, which would not follow a later change to `defaultPlan`).
|
|
119
120
|
|
|
120
|
-
Overrides accumulate: each `override()` call merges into the subject's existing override rather than replacing it, so granting more seats and later enabling a feature leaves both in place, and overriding a key again updates just that key. `clearOverride(subject)` removes the whole override; `clearOverride(subject, { limits: ["seats"] })` reverts only the named keys and keeps the rest.
|
|
121
|
+
Overrides accumulate: each `override()` call merges into the subject's existing override rather than replacing it, so granting more seats and later enabling a feature leaves both in place, and overriding a key again updates just that key. The merge happens in the database under a row lock, so two concurrent overrides to the same subject both land instead of one clobbering the other. `clearOverride(subject)` removes the whole override; `clearOverride(subject, { limits: ["seats"] })` reverts only the named keys and keeps the rest.
|
|
121
122
|
|
|
122
123
|
```js
|
|
123
124
|
await entitlements.override(account.id, { limits: { seats: 50 } })
|
|
@@ -132,7 +133,7 @@ Resolved entitlements are cached per subject for a short, configurable window (`
|
|
|
132
133
|
|
|
133
134
|
### `createEntitlements({ driver, plans, defaultPlan, features?, limits?, meter?, cacheTtl?, tracer? })`
|
|
134
135
|
|
|
135
|
-
Creates a resolver. `driver` is `postgresDriver({ pool })` or `memoryDriver()`. `plans` is the catalog; `defaultPlan` must be one of its keys. `features` and `limits` declare the universe of valid keys (defaulting to the union across plans); when given, plans may only reference declared keys. `meter` is an optional `@prsm/meter` instance, required only for `check()`. `cacheTtl` accepts ms or a string like `"10s"`. `tracer` is an optional `@prsm/trace` tracer.
|
|
136
|
+
Creates a resolver. `driver` is `postgresDriver({ pool })` or `memoryDriver()`. `plans` is the catalog; `defaultPlan` must be one of its keys. `features` and `limits` declare the universe of valid keys (defaulting to the union across plans); when given, plans may only reference declared keys. `meter` is an optional `@prsm/meter` instance, required only for `check()`. `cacheTtl` accepts ms or a string like `"10s"`. `tracer` is an optional `@prsm/trace` tracer; `can()` and `check()` are wrapped in spans when it is present.
|
|
136
137
|
|
|
137
138
|
### `entitlements.setup()`
|
|
138
139
|
|
package/package.json
CHANGED
package/src/entitlements.js
CHANGED
|
@@ -226,11 +226,7 @@ export function createEntitlements(options = {}) {
|
|
|
226
226
|
async override(subject, data) {
|
|
227
227
|
if (!subject) throw new Error("override requires a `subject`")
|
|
228
228
|
validateOverride(data, featureUniverse, limitUniverse)
|
|
229
|
-
|
|
230
|
-
await driver.setOverride(subject, {
|
|
231
|
-
features: { ...(current.features ?? {}), ...(data.features ?? {}) },
|
|
232
|
-
limits: { ...(current.limits ?? {}), ...(data.limits ?? {}) },
|
|
233
|
-
})
|
|
229
|
+
await driver.mergeOverride(subject, { features: data.features ?? {}, limits: data.limits ?? {} })
|
|
234
230
|
cache.invalidate(subject)
|
|
235
231
|
},
|
|
236
232
|
|
|
@@ -245,20 +241,8 @@ export function createEntitlements(options = {}) {
|
|
|
245
241
|
if (!subject) throw new Error("clearOverride requires a `subject`")
|
|
246
242
|
if (!keys) {
|
|
247
243
|
await driver.clearOverride(subject)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
const current = (await driver.getState(subject))?.override
|
|
252
|
-
if (current) {
|
|
253
|
-
const features = { ...(current.features ?? {}) }
|
|
254
|
-
const limits = { ...(current.limits ?? {}) }
|
|
255
|
-
for (const k of keys.features ?? []) delete features[k]
|
|
256
|
-
for (const k of keys.limits ?? []) delete limits[k]
|
|
257
|
-
if (Object.keys(features).length === 0 && Object.keys(limits).length === 0) {
|
|
258
|
-
await driver.clearOverride(subject)
|
|
259
|
-
} else {
|
|
260
|
-
await driver.setOverride(subject, { features, limits })
|
|
261
|
-
}
|
|
244
|
+
} else {
|
|
245
|
+
await driver.removeOverrideKeys(subject, { features: keys.features ?? [], limits: keys.limits ?? [] })
|
|
262
246
|
}
|
|
263
247
|
cache.invalidate(subject)
|
|
264
248
|
},
|
package/src/memoryDriver.js
CHANGED
|
@@ -27,8 +27,22 @@ export function memoryDriver() {
|
|
|
27
27
|
assignments.delete(subject)
|
|
28
28
|
},
|
|
29
29
|
|
|
30
|
-
async
|
|
31
|
-
overrides.
|
|
30
|
+
async mergeOverride(subject, delta) {
|
|
31
|
+
const current = overrides.get(subject) ?? { features: {}, limits: {} }
|
|
32
|
+
overrides.set(subject, {
|
|
33
|
+
features: { ...(current.features ?? {}), ...(delta.features ?? {}) },
|
|
34
|
+
limits: { ...(current.limits ?? {}), ...(delta.limits ?? {}) },
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async removeOverrideKeys(subject, keys) {
|
|
39
|
+
const current = overrides.get(subject)
|
|
40
|
+
if (!current) return
|
|
41
|
+
const features = { ...(current.features ?? {}) }
|
|
42
|
+
const limits = { ...(current.limits ?? {}) }
|
|
43
|
+
for (const k of keys.features ?? []) delete features[k]
|
|
44
|
+
for (const k of keys.limits ?? []) delete limits[k]
|
|
45
|
+
overrides.set(subject, { features, limits })
|
|
32
46
|
},
|
|
33
47
|
|
|
34
48
|
async clearOverride(subject) {
|
package/src/postgresDriver.js
CHANGED
|
@@ -56,11 +56,30 @@ export function postgresDriver(options = {}) {
|
|
|
56
56
|
await pool.query(`delete from ${assignments} where subject = $1`, [subject])
|
|
57
57
|
},
|
|
58
58
|
|
|
59
|
-
async
|
|
59
|
+
async mergeOverride(subject, delta) {
|
|
60
60
|
await pool.query(
|
|
61
|
-
`insert into ${overrides} (subject, data)
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
`insert into ${overrides} (subject, data)
|
|
62
|
+
values ($1, jsonb_build_object('features', $2::jsonb, 'limits', $3::jsonb))
|
|
63
|
+
on conflict (subject) do update set
|
|
64
|
+
data = jsonb_build_object(
|
|
65
|
+
'features', coalesce(${overrides}.data -> 'features', '{}'::jsonb) || (excluded.data -> 'features'),
|
|
66
|
+
'limits', coalesce(${overrides}.data -> 'limits', '{}'::jsonb) || (excluded.data -> 'limits')
|
|
67
|
+
),
|
|
68
|
+
updated_at = now()`,
|
|
69
|
+
[subject, JSON.stringify(delta.features ?? {}), JSON.stringify(delta.limits ?? {})],
|
|
70
|
+
)
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async removeOverrideKeys(subject, keys) {
|
|
74
|
+
await pool.query(
|
|
75
|
+
`update ${overrides} set
|
|
76
|
+
data = jsonb_build_object(
|
|
77
|
+
'features', coalesce(data -> 'features', '{}'::jsonb) - $2::text[],
|
|
78
|
+
'limits', coalesce(data -> 'limits', '{}'::jsonb) - $3::text[]
|
|
79
|
+
),
|
|
80
|
+
updated_at = now()
|
|
81
|
+
where subject = $1`,
|
|
82
|
+
[subject, keys.features ?? [], keys.limits ?? []],
|
|
64
83
|
)
|
|
65
84
|
},
|
|
66
85
|
|