@prsm/entitle 2.2.0 → 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 CHANGED
@@ -175,6 +175,10 @@ 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
+
178
182
  ### `entitlements.catalog()`
179
183
 
180
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prsm/entitle",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Plan-based entitlements and feature gating, resolved at runtime, backed by postgres",
5
5
  "type": "module",
6
6
  "exports": {
@@ -357,6 +357,19 @@ export function createEntitlements(options = {}) {
357
357
  })
358
358
  },
359
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
+
360
373
  /**
361
374
  * The subject's full effective entitlements, for a settings or billing page.
362
375
  * @param {string} subject - the subject identifier to resolve
@@ -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() {},
@@ -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)
@@ -93,6 +93,22 @@ export function createEntitlements(options?: EntitlementsOptions): {
93
93
  period?: any;
94
94
  range?: any;
95
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
+ }>>;
96
112
  /**
97
113
  * The subject's full effective entitlements, for a settings or billing page.
98
114
  * @param {string} subject - the subject identifier to resolve