@prsm/entitle 1.0.0 → 1.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 CHANGED
@@ -104,6 +104,15 @@ The **plan catalog** is declared once at construction, the way you declare your
104
104
 
105
105
  **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`.
106
106
 
107
+ 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.
108
+
109
+ ```js
110
+ await entitlements.override(account.id, { limits: { seats: 50 } })
111
+ await entitlements.override(account.id, { features: { sso: true } }) // seats override stays
112
+ await entitlements.clearOverride(account.id, { limits: ["seats"] }) // seats reverts to plan, sso stays
113
+ await entitlements.clearOverride(account.id) // back to plain plan
114
+ ```
115
+
107
116
  Resolved entitlements are cached per subject for a short, configurable window (`cacheTtl`, default `"10s"`) and the cache is invalidated immediately on `assign`, `override`, and `clearOverride`, so the instance making a change sees it at once and other instances converge within the TTL. Set `cacheTtl: 0` to read postgres on every call.
108
117
 
109
118
  ## API
@@ -124,9 +133,9 @@ Assigns a subject to a plan. Takes effect immediately.
124
133
 
125
134
  Layers a per-subject override on top of the plan. Shallow-merges; pass only what differs.
126
135
 
127
- ### `entitlements.clearOverride(subject)`
136
+ ### `entitlements.clearOverride(subject, keys?)`
128
137
 
129
- Removes the subject's override.
138
+ With no `keys`, removes the subject's entire override. With `keys` (`{ features?: string[], limits?: string[] }`), removes only those entries and keeps the rest.
130
139
 
131
140
  ### `entitlements.can(subject, feature)`
132
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prsm/entitle",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Plan-based entitlements and feature gating, resolved at runtime, backed by postgres",
5
5
  "type": "module",
6
6
  "exports": {
@@ -156,25 +156,51 @@ export function createEntitlements(options = {}) {
156
156
  },
157
157
 
158
158
  /**
159
- * Layer a per-subject override on top of the subject's plan. Shallow-merges,
160
- * so pass only what differs. Takes effect immediately.
159
+ * Add or adjust a per-subject override, layered on top of the subject's plan.
160
+ * Merges into any existing override for the subject, so repeated calls
161
+ * accumulate: overriding `seats`, then later overriding `sso`, leaves both in
162
+ * place, and overriding a key again updates just that key. Use clearOverride
163
+ * to remove overrides. Takes effect immediately.
161
164
  * @param {string} subject
162
165
  * @param {Override} data
163
166
  */
164
167
  async override(subject, data) {
165
168
  if (!subject) throw new Error("override requires a `subject`")
166
169
  validateOverride(data)
167
- await driver.setOverride(subject, { features: data.features ?? {}, limits: data.limits ?? {} })
170
+ const current = (await driver.getState(subject))?.override ?? {}
171
+ await driver.setOverride(subject, {
172
+ features: { ...(current.features ?? {}), ...(data.features ?? {}) },
173
+ limits: { ...(current.limits ?? {}), ...(data.limits ?? {}) },
174
+ })
168
175
  cache.invalidate(subject)
169
176
  },
170
177
 
171
178
  /**
172
- * Remove a subject's override, falling back to plain plan entitlements.
179
+ * Remove overrides for a subject. With no `keys`, removes the entire override
180
+ * and the subject falls back to plain plan entitlements. With `keys`, removes
181
+ * only those override entries (reverting them to the plan) and keeps the rest.
173
182
  * @param {string} subject
183
+ * @param {{ features?: string[], limits?: string[] }} [keys]
174
184
  */
175
- async clearOverride(subject) {
185
+ async clearOverride(subject, keys) {
176
186
  if (!subject) throw new Error("clearOverride requires a `subject`")
177
- await driver.clearOverride(subject)
187
+ if (!keys) {
188
+ await driver.clearOverride(subject)
189
+ cache.invalidate(subject)
190
+ return
191
+ }
192
+ const current = (await driver.getState(subject))?.override
193
+ if (current) {
194
+ const features = { ...(current.features ?? {}) }
195
+ const limits = { ...(current.limits ?? {}) }
196
+ for (const k of keys.features ?? []) delete features[k]
197
+ for (const k of keys.limits ?? []) delete limits[k]
198
+ if (Object.keys(features).length === 0 && Object.keys(limits).length === 0) {
199
+ await driver.clearOverride(subject)
200
+ } else {
201
+ await driver.setOverride(subject, { features, limits })
202
+ }
203
+ }
178
204
  cache.invalidate(subject)
179
205
  },
180
206
 
@@ -16,17 +16,26 @@ export function createEntitlements(options?: EntitlementsOptions): {
16
16
  */
17
17
  assign(subject: string, plan: string): Promise<void>;
18
18
  /**
19
- * Layer a per-subject override on top of the subject's plan. Shallow-merges,
20
- * so pass only what differs. Takes effect immediately.
19
+ * Add or adjust a per-subject override, layered on top of the subject's plan.
20
+ * Merges into any existing override for the subject, so repeated calls
21
+ * accumulate: overriding `seats`, then later overriding `sso`, leaves both in
22
+ * place, and overriding a key again updates just that key. Use clearOverride
23
+ * to remove overrides. Takes effect immediately.
21
24
  * @param {string} subject
22
25
  * @param {Override} data
23
26
  */
24
27
  override(subject: string, data: Override): Promise<void>;
25
28
  /**
26
- * Remove a subject's override, falling back to plain plan entitlements.
29
+ * Remove overrides for a subject. With no `keys`, removes the entire override
30
+ * and the subject falls back to plain plan entitlements. With `keys`, removes
31
+ * only those override entries (reverting them to the plan) and keeps the rest.
27
32
  * @param {string} subject
33
+ * @param {{ features?: string[], limits?: string[] }} [keys]
28
34
  */
29
- clearOverride(subject: string): Promise<void>;
35
+ clearOverride(subject: string, keys?: {
36
+ features?: string[];
37
+ limits?: string[];
38
+ }): Promise<void>;
30
39
  /**
31
40
  * The subject's effective plan name (the default plan if unassigned).
32
41
  * @param {string} subject