@prsm/entitle 2.1.2 → 2.3.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 +8 -0
- package/package.json +1 -1
- package/src/entitlements.js +44 -0
- package/src/memoryDriver.js +21 -0
- package/src/postgresDriver.js +22 -0
- package/types/entitlements.d.ts +44 -0
package/README.md
CHANGED
|
@@ -175,6 +175,14 @@ Returns the subject's effective plan name.
|
|
|
175
175
|
|
|
176
176
|
Returns the full effective snapshot `{ plan, features, limits }`, for a settings or billing page.
|
|
177
177
|
|
|
178
|
+
### `entitlements.subjects({ limit? })`
|
|
179
|
+
|
|
180
|
+
Lists subjects that have an explicit assignment or override, most-recently-configured first, capped at `limit` (default `100`). Subjects on the default plan with no override are never stored, so they do not appear - this lists the configured subjects, for discovery in dashboards and admin tools. Returns `[{ subject, assigned, overridden, lastConfiguredAt }]`.
|
|
181
|
+
|
|
182
|
+
### `entitlements.catalog()`
|
|
183
|
+
|
|
184
|
+
Returns the static configuration `{ defaultPlan, plans, features, limits }`: every declared plan, the default plan, and the full feature and limit universes. Where `describe` resolves a single subject, `catalog` exposes the whole offering, for a plan comparison table, an admin dashboard, or documenting what the system grants. Subject-independent and read-only, so it never touches storage. The returned object is a fresh copy.
|
|
185
|
+
|
|
178
186
|
### `entitlements.close()`
|
|
179
187
|
|
|
180
188
|
Releases driver resources.
|
package/package.json
CHANGED
package/src/entitlements.js
CHANGED
|
@@ -34,6 +34,14 @@ import ms from "@prsm/ms"
|
|
|
34
34
|
* @property {Record<string, number|null>} limits - the resolved limit ceilings, after applying the subject's override on top of the plan (`null` means unlimited)
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} Catalog
|
|
39
|
+
* @property {string} defaultPlan - the plan applied to a subject with no assignment
|
|
40
|
+
* @property {Record<string, Plan>} plans - the declared plan catalog (your pricing tiers)
|
|
41
|
+
* @property {string[]} features - the full feature universe (declared, or derived from the union across plans)
|
|
42
|
+
* @property {string[]} limits - the full limit universe (declared, or derived from the union across plans)
|
|
43
|
+
*/
|
|
44
|
+
|
|
37
45
|
/**
|
|
38
46
|
* @typedef {Object} CheckResult
|
|
39
47
|
* @property {boolean} allowed - whether current usage is below the limit (always true when the limit is unlimited)
|
|
@@ -200,6 +208,29 @@ export function createEntitlements(options = {}) {
|
|
|
200
208
|
return driver.setup()
|
|
201
209
|
},
|
|
202
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The static configuration of this resolver: the plan catalog, the default
|
|
213
|
+
* plan, and the full feature and limit universes. Unlike describe(), which
|
|
214
|
+
* resolves a single subject, this exposes every plan and every declared key
|
|
215
|
+
* - for plan comparison tables, admin dashboards, or documenting what the
|
|
216
|
+
* system offers. Subject-independent, so it never touches storage. The
|
|
217
|
+
* returned object is a fresh copy; mutating it does not affect the resolver.
|
|
218
|
+
* @returns {Catalog}
|
|
219
|
+
*/
|
|
220
|
+
catalog() {
|
|
221
|
+
return {
|
|
222
|
+
defaultPlan,
|
|
223
|
+
plans: Object.fromEntries(
|
|
224
|
+
Object.entries(catalog).map(([name, p]) => [name, {
|
|
225
|
+
features: { ...(p.features ?? {}) },
|
|
226
|
+
limits: { ...(p.limits ?? {}) },
|
|
227
|
+
}]),
|
|
228
|
+
),
|
|
229
|
+
features: [...featureUniverse],
|
|
230
|
+
limits: [...limitUniverse],
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
203
234
|
/**
|
|
204
235
|
* Assign a subject to a plan. Takes effect immediately.
|
|
205
236
|
* @param {string} subject - the subject identifier (whatever id you key entitlements by, such as an account or user id)
|
|
@@ -326,6 +357,19 @@ export function createEntitlements(options = {}) {
|
|
|
326
357
|
})
|
|
327
358
|
},
|
|
328
359
|
|
|
360
|
+
/**
|
|
361
|
+
* List subjects that have an explicit assignment or override, most-recently-
|
|
362
|
+
* configured first, capped at `limit` (default 100). Subjects on the default
|
|
363
|
+
* plan with no override are not stored, so they do not appear - this lists
|
|
364
|
+
* the configured subjects, for discovery in dashboards and admin tools.
|
|
365
|
+
* @param {{ limit?: number }} [query]
|
|
366
|
+
* @returns {Promise<Array<{ subject: string, assigned: boolean, overridden: boolean, lastConfiguredAt: Date|null }>>}
|
|
367
|
+
*/
|
|
368
|
+
async subjects(query = {}) {
|
|
369
|
+
if (!driver.subjects) throw new Error("this driver does not support listing subjects")
|
|
370
|
+
return driver.subjects({ limit: query.limit ?? 100 })
|
|
371
|
+
},
|
|
372
|
+
|
|
329
373
|
/**
|
|
330
374
|
* The subject's full effective entitlements, for a settings or billing page.
|
|
331
375
|
* @param {string} subject - the subject identifier to resolve
|
package/src/memoryDriver.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
export function memoryDriver() {
|
|
9
9
|
const assignments = new Map()
|
|
10
10
|
const overrides = new Map()
|
|
11
|
+
const touched = new Map()
|
|
12
|
+
|
|
13
|
+
const touch = (subject) => touched.set(subject, new Date())
|
|
11
14
|
|
|
12
15
|
return {
|
|
13
16
|
async setup() {},
|
|
@@ -19,12 +22,27 @@ export function memoryDriver() {
|
|
|
19
22
|
}
|
|
20
23
|
},
|
|
21
24
|
|
|
25
|
+
async subjects({ limit }) {
|
|
26
|
+
const subs = new Set([...assignments.keys(), ...overrides.keys()])
|
|
27
|
+
return [...subs]
|
|
28
|
+
.map((subject) => ({
|
|
29
|
+
subject,
|
|
30
|
+
assigned: assignments.has(subject),
|
|
31
|
+
overridden: overrides.has(subject),
|
|
32
|
+
lastConfiguredAt: touched.get(subject) ?? null,
|
|
33
|
+
}))
|
|
34
|
+
.sort((a, b) => (b.lastConfiguredAt?.getTime() ?? 0) - (a.lastConfiguredAt?.getTime() ?? 0) || (a.subject < b.subject ? -1 : 1))
|
|
35
|
+
.slice(0, limit)
|
|
36
|
+
},
|
|
37
|
+
|
|
22
38
|
async assign(subject, plan) {
|
|
23
39
|
assignments.set(subject, plan)
|
|
40
|
+
touch(subject)
|
|
24
41
|
},
|
|
25
42
|
|
|
26
43
|
async unassign(subject) {
|
|
27
44
|
assignments.delete(subject)
|
|
45
|
+
touch(subject)
|
|
28
46
|
},
|
|
29
47
|
|
|
30
48
|
async mergeOverride(subject, delta) {
|
|
@@ -33,6 +51,7 @@ export function memoryDriver() {
|
|
|
33
51
|
features: { ...(current.features ?? {}), ...(delta.features ?? {}) },
|
|
34
52
|
limits: { ...(current.limits ?? {}), ...(delta.limits ?? {}) },
|
|
35
53
|
})
|
|
54
|
+
touch(subject)
|
|
36
55
|
},
|
|
37
56
|
|
|
38
57
|
async removeOverrideKeys(subject, keys) {
|
|
@@ -43,10 +62,12 @@ export function memoryDriver() {
|
|
|
43
62
|
for (const k of keys.features ?? []) delete features[k]
|
|
44
63
|
for (const k of keys.limits ?? []) delete limits[k]
|
|
45
64
|
overrides.set(subject, { features, limits })
|
|
65
|
+
touch(subject)
|
|
46
66
|
},
|
|
47
67
|
|
|
48
68
|
async clearOverride(subject) {
|
|
49
69
|
overrides.delete(subject)
|
|
70
|
+
touch(subject)
|
|
50
71
|
},
|
|
51
72
|
|
|
52
73
|
async close() {},
|
package/src/postgresDriver.js
CHANGED
|
@@ -44,6 +44,28 @@ export function postgresDriver(options = {}) {
|
|
|
44
44
|
return { plan: r.rows[0].plan ?? null, override: r.rows[0].override ?? null }
|
|
45
45
|
},
|
|
46
46
|
|
|
47
|
+
async subjects({ limit }) {
|
|
48
|
+
const r = await pool.query(
|
|
49
|
+
`select subject,
|
|
50
|
+
bool_or(src = 'a') as assigned,
|
|
51
|
+
bool_or(src = 'o') as overridden,
|
|
52
|
+
max(updated_at) as last_at
|
|
53
|
+
from (
|
|
54
|
+
select subject, updated_at, 'a' as src from ${assignments}
|
|
55
|
+
union all
|
|
56
|
+
select subject, updated_at, 'o' as src from ${overrides}
|
|
57
|
+
) u
|
|
58
|
+
group by subject order by last_at desc, subject asc limit $1`,
|
|
59
|
+
[limit],
|
|
60
|
+
)
|
|
61
|
+
return r.rows.map((row) => ({
|
|
62
|
+
subject: row.subject,
|
|
63
|
+
assigned: row.assigned,
|
|
64
|
+
overridden: row.overridden,
|
|
65
|
+
lastConfiguredAt: row.last_at,
|
|
66
|
+
}))
|
|
67
|
+
},
|
|
68
|
+
|
|
47
69
|
async assign(subject, plan) {
|
|
48
70
|
await pool.query(
|
|
49
71
|
`insert into ${assignments} (subject, plan) values ($1, $2)
|
package/types/entitlements.d.ts
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
export function createEntitlements(options?: EntitlementsOptions): {
|
|
10
10
|
/** Create the backing tables if they do not exist. Idempotent. */
|
|
11
11
|
setup(): any;
|
|
12
|
+
/**
|
|
13
|
+
* The static configuration of this resolver: the plan catalog, the default
|
|
14
|
+
* plan, and the full feature and limit universes. Unlike describe(), which
|
|
15
|
+
* resolves a single subject, this exposes every plan and every declared key
|
|
16
|
+
* - for plan comparison tables, admin dashboards, or documenting what the
|
|
17
|
+
* system offers. Subject-independent, so it never touches storage. The
|
|
18
|
+
* returned object is a fresh copy; mutating it does not affect the resolver.
|
|
19
|
+
* @returns {Catalog}
|
|
20
|
+
*/
|
|
21
|
+
catalog(): Catalog;
|
|
12
22
|
/**
|
|
13
23
|
* Assign a subject to a plan. Takes effect immediately.
|
|
14
24
|
* @param {string} subject - the subject identifier (whatever id you key entitlements by, such as an account or user id)
|
|
@@ -83,6 +93,22 @@ export function createEntitlements(options?: EntitlementsOptions): {
|
|
|
83
93
|
period?: any;
|
|
84
94
|
range?: any;
|
|
85
95
|
}): Promise<CheckResult>;
|
|
96
|
+
/**
|
|
97
|
+
* List subjects that have an explicit assignment or override, most-recently-
|
|
98
|
+
* configured first, capped at `limit` (default 100). Subjects on the default
|
|
99
|
+
* plan with no override are not stored, so they do not appear - this lists
|
|
100
|
+
* the configured subjects, for discovery in dashboards and admin tools.
|
|
101
|
+
* @param {{ limit?: number }} [query]
|
|
102
|
+
* @returns {Promise<Array<{ subject: string, assigned: boolean, overridden: boolean, lastConfiguredAt: Date|null }>>}
|
|
103
|
+
*/
|
|
104
|
+
subjects(query?: {
|
|
105
|
+
limit?: number;
|
|
106
|
+
}): Promise<Array<{
|
|
107
|
+
subject: string;
|
|
108
|
+
assigned: boolean;
|
|
109
|
+
overridden: boolean;
|
|
110
|
+
lastConfiguredAt: Date | null;
|
|
111
|
+
}>>;
|
|
86
112
|
/**
|
|
87
113
|
* The subject's full effective entitlements, for a settings or billing page.
|
|
88
114
|
* @param {string} subject - the subject identifier to resolve
|
|
@@ -169,6 +195,24 @@ export type Effective = {
|
|
|
169
195
|
*/
|
|
170
196
|
limits: Record<string, number | null>;
|
|
171
197
|
};
|
|
198
|
+
export type Catalog = {
|
|
199
|
+
/**
|
|
200
|
+
* - the plan applied to a subject with no assignment
|
|
201
|
+
*/
|
|
202
|
+
defaultPlan: string;
|
|
203
|
+
/**
|
|
204
|
+
* - the declared plan catalog (your pricing tiers)
|
|
205
|
+
*/
|
|
206
|
+
plans: Record<string, Plan>;
|
|
207
|
+
/**
|
|
208
|
+
* - the full feature universe (declared, or derived from the union across plans)
|
|
209
|
+
*/
|
|
210
|
+
features: string[];
|
|
211
|
+
/**
|
|
212
|
+
* - the full limit universe (declared, or derived from the union across plans)
|
|
213
|
+
*/
|
|
214
|
+
limits: string[];
|
|
215
|
+
};
|
|
172
216
|
export type CheckResult = {
|
|
173
217
|
/**
|
|
174
218
|
* - whether current usage is below the limit (always true when the limit is unlimited)
|