@brandtg/flapjack 1.2.0 → 1.4.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/dist/model.d.ts CHANGED
@@ -170,6 +170,23 @@ export declare class FeatureFlagModel {
170
170
  * ```
171
171
  */
172
172
  delete(id: number): Promise<boolean>;
173
+ /**
174
+ * Adds an external subject identifier to a feature flag.
175
+ */
176
+ addSubject(featureFlagId: number, subject: string): Promise<boolean>;
177
+ /**
178
+ * Removes an external subject identifier from a feature flag.
179
+ */
180
+ removeSubject(featureFlagId: number, subject: string): Promise<boolean>;
181
+ /**
182
+ * Gets all external subject identifiers associated with a feature flag.
183
+ */
184
+ getSubjects(featureFlagId: number): Promise<string[]>;
185
+ /**
186
+ * Gets all feature flags directly associated with an external subject identifier.
187
+ */
188
+ getFeatureFlagsForSubject(subject: string): Promise<FeatureFlag[]>;
189
+ private getSubjectMatchedFlagIds;
173
190
  /**
174
191
  * Checks if a user belongs to any of the specified groups
175
192
  */
@@ -178,10 +195,12 @@ export declare class FeatureFlagModel {
178
195
  * Evaluates a feature flag for a user based on flag configuration.
179
196
  * This is a stateless helper that performs the actual flag evaluation logic.
180
197
  */
181
- evaluateFlagForUser(flag: FeatureFlag, { user, roles, groups, }: {
198
+ evaluateFlagForUser(flag: FeatureFlag, { user, roles, groups, subjects, subjectMatchedFlagIds, }: {
182
199
  user?: string;
183
200
  roles?: string[];
184
201
  groups?: string[];
202
+ subjects?: string[];
203
+ subjectMatchedFlagIds?: Set<number>;
185
204
  }): Promise<boolean>;
186
205
  /**
187
206
  * Computes the hash value for a user ID using MurmurHash3.
@@ -245,6 +264,16 @@ export declare class FeatureFlagModel {
245
264
  roles?: string[];
246
265
  groups?: string[];
247
266
  }): Promise<boolean>;
267
+ /**
268
+ * Checks if a feature flag is active for a context, including optional external subjects.
269
+ */
270
+ isActiveForContext({ name, user, roles, groups, subjects, }: {
271
+ name: string;
272
+ user?: string;
273
+ roles?: string[];
274
+ groups?: string[];
275
+ subjects?: string[];
276
+ }): Promise<boolean>;
248
277
  /**
249
278
  * Checks if multiple feature flags are active for a user based on configured rules.
250
279
  *
@@ -298,6 +327,32 @@ export declare class FeatureFlagModel {
298
327
  roles?: string[];
299
328
  groups?: string[];
300
329
  }): Promise<Record<string, boolean>>;
330
+ /**
331
+ * Checks if multiple feature flags are active for a context, including optional external subjects.
332
+ */
333
+ areActiveForContext({ names, user, roles, groups, subjects, }: {
334
+ names?: string[];
335
+ user?: string;
336
+ roles?: string[];
337
+ groups?: string[];
338
+ subjects?: string[];
339
+ }): Promise<Record<string, boolean>>;
340
+ /**
341
+ * Gets the latest modified timestamp across all feature flags.
342
+ *
343
+ * @returns The latest modified timestamp in milliseconds since epoch, or -1 if the table is empty
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * const lastModified = await model.getLastModified();
348
+ * if (lastModified === -1) {
349
+ * console.log("No feature flags exist");
350
+ * } else {
351
+ * console.log(`Last modified: ${new Date(lastModified)}`);
352
+ * }
353
+ * ```
354
+ */
355
+ getLastModified(): Promise<number>;
301
356
  }
302
357
  type CreateGroupInput = Omit<FeatureFlagGroup, "id" | "created" | "modified">;
303
358
  type UpdateGroupChanges = Partial<Omit<FeatureFlagGroup, "id" | "created" | "modified">>;
@@ -470,6 +525,22 @@ export declare class FeatureFlagGroupModel {
470
525
  * ```
471
526
  */
472
527
  getGroupsForFeatureFlag(featureFlagId: number): Promise<FeatureFlagGroup[]>;
528
+ /**
529
+ * Adds an external subject identifier to a feature flag group.
530
+ */
531
+ addSubject(groupId: number, subject: string): Promise<boolean>;
532
+ /**
533
+ * Removes an external subject identifier from a feature flag group.
534
+ */
535
+ removeSubject(groupId: number, subject: string): Promise<boolean>;
536
+ /**
537
+ * Gets all external subject identifiers associated with a feature flag group.
538
+ */
539
+ getSubjects(groupId: number): Promise<string[]>;
540
+ /**
541
+ * Gets all groups that are associated with a specific external subject identifier.
542
+ */
543
+ getGroupsForSubject(subject: string): Promise<FeatureFlagGroup[]>;
473
544
  /**
474
545
  * Updates all feature flags in a group with the same changes.
475
546
  *
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
package/dist/model.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import MurmurHash3 from "imurmurhash";
2
2
  const TABLE = "flapjack.feature_flag";
3
+ const SUBJECT_TABLE = "flapjack.feature_flag_subject";
4
+ const GROUP_SUBJECT_TABLE = "flapjack.feature_flag_group_subject";
3
5
  const COLUMNS = [
4
6
  "id",
5
7
  "name",
@@ -311,6 +313,69 @@ export class FeatureFlagModel {
311
313
  const res = await this.db.query(`DELETE FROM ${TABLE} WHERE id = $1`, [id]);
312
314
  return res.rowCount > 0;
313
315
  }
316
+ /**
317
+ * Adds an external subject identifier to a feature flag.
318
+ */
319
+ async addSubject(featureFlagId, subject) {
320
+ try {
321
+ const sql = `INSERT INTO ${SUBJECT_TABLE} (feature_flag_id, subject) VALUES ($1, $2)`;
322
+ await this.db.query(sql, [featureFlagId, subject]);
323
+ return true;
324
+ }
325
+ catch (err) {
326
+ if (err.code === "23505") {
327
+ return false;
328
+ }
329
+ throw err;
330
+ }
331
+ }
332
+ /**
333
+ * Removes an external subject identifier from a feature flag.
334
+ */
335
+ async removeSubject(featureFlagId, subject) {
336
+ const sql = `DELETE FROM ${SUBJECT_TABLE} WHERE feature_flag_id = $1 AND subject = $2`;
337
+ const res = await this.db.query(sql, [featureFlagId, subject]);
338
+ return res.rowCount > 0;
339
+ }
340
+ /**
341
+ * Gets all external subject identifiers associated with a feature flag.
342
+ */
343
+ async getSubjects(featureFlagId) {
344
+ const sql = `SELECT subject FROM ${SUBJECT_TABLE} WHERE feature_flag_id = $1 ORDER BY created DESC`;
345
+ const res = await this.db.query(sql, [featureFlagId]);
346
+ return res.rows.map((row) => row.subject);
347
+ }
348
+ /**
349
+ * Gets all feature flags directly associated with an external subject identifier.
350
+ */
351
+ async getFeatureFlagsForSubject(subject) {
352
+ const sql = `
353
+ SELECT ${COLUMNS.map((c) => `f.${c}`).join(", ")}
354
+ FROM ${TABLE} f
355
+ INNER JOIN ${SUBJECT_TABLE} s ON f.id = s.feature_flag_id
356
+ WHERE s.subject = $1
357
+ ORDER BY f.created DESC
358
+ `;
359
+ const res = await this.db.query(sql, [subject]);
360
+ return res.rows.map(mapRow);
361
+ }
362
+ async getSubjectMatchedFlagIds(subjects = []) {
363
+ if (subjects.length === 0) {
364
+ return new Set();
365
+ }
366
+ const sql = `
367
+ SELECT s.feature_flag_id AS feature_flag_id
368
+ FROM ${SUBJECT_TABLE} s
369
+ WHERE s.subject = ANY($1)
370
+ UNION
371
+ SELECT gm.feature_flag_id AS feature_flag_id
372
+ FROM ${GROUP_SUBJECT_TABLE} gs
373
+ INNER JOIN ${GROUP_MEMBER_TABLE} gm ON gs.feature_flag_group_id = gm.group_id
374
+ WHERE gs.subject = ANY($1)
375
+ `;
376
+ const res = await this.db.query(sql, [subjects]);
377
+ return new Set(res.rows.map((row) => Number(row.feature_flag_id)));
378
+ }
314
379
  /**
315
380
  * Checks if a user belongs to any of the specified groups
316
381
  */
@@ -323,7 +388,7 @@ export class FeatureFlagModel {
323
388
  * Evaluates a feature flag for a user based on flag configuration.
324
389
  * This is a stateless helper that performs the actual flag evaluation logic.
325
390
  */
326
- async evaluateFlagForUser(flag, { user, roles, groups, }) {
391
+ async evaluateFlagForUser(flag, { user, roles, groups, subjects, subjectMatchedFlagIds, }) {
327
392
  // Everyone Override: If everyone is true or false, return that value immediately
328
393
  if (flag.everyone !== undefined && flag.everyone !== null) {
329
394
  return flag.everyone;
@@ -332,6 +397,20 @@ export class FeatureFlagModel {
332
397
  if (user && flag.users && flag.users.includes(user)) {
333
398
  return true;
334
399
  }
400
+ // Subject Check: If any external subject maps to this flag directly or via group, return true
401
+ if (subjects && subjects.length > 0) {
402
+ let isSubjectMatched = false;
403
+ if (subjectMatchedFlagIds) {
404
+ isSubjectMatched = subjectMatchedFlagIds.has(flag.id);
405
+ }
406
+ else {
407
+ const matchedFlagIds = await this.getSubjectMatchedFlagIds(subjects);
408
+ isSubjectMatched = matchedFlagIds.has(flag.id);
409
+ }
410
+ if (isSubjectMatched) {
411
+ return true;
412
+ }
413
+ }
335
414
  // Group Check: If any of the user's groups match any group in the groups array, return true
336
415
  if (groups && this.isActiveForGroups(groups, flag.groups)) {
337
416
  return true;
@@ -414,6 +493,17 @@ export class FeatureFlagModel {
414
493
  * ```
415
494
  */
416
495
  async isActiveForUser({ name, user, roles, groups, }) {
496
+ return this.isActiveForContext({
497
+ name,
498
+ user,
499
+ roles,
500
+ groups,
501
+ });
502
+ }
503
+ /**
504
+ * Checks if a feature flag is active for a context, including optional external subjects.
505
+ */
506
+ async isActiveForContext({ name, user, roles, groups, subjects, }) {
417
507
  const flag = await this.getByName(name);
418
508
  // No such flag
419
509
  if (!flag) {
@@ -429,7 +519,14 @@ export class FeatureFlagModel {
429
519
  return expiredResult;
430
520
  }
431
521
  }
432
- return this.evaluateFlagForUser(flag, { user, roles, groups });
522
+ const subjectMatchedFlagIds = await this.getSubjectMatchedFlagIds(subjects);
523
+ return this.evaluateFlagForUser(flag, {
524
+ user,
525
+ roles,
526
+ groups,
527
+ subjects,
528
+ subjectMatchedFlagIds,
529
+ });
433
530
  }
434
531
  /**
435
532
  * Checks if multiple feature flags are active for a user based on configured rules.
@@ -479,6 +576,17 @@ export class FeatureFlagModel {
479
576
  * ```
480
577
  */
481
578
  async areActiveForUser({ names, user, roles, groups, }) {
579
+ return this.areActiveForContext({
580
+ names,
581
+ user,
582
+ roles,
583
+ groups,
584
+ });
585
+ }
586
+ /**
587
+ * Checks if multiple feature flags are active for a context, including optional external subjects.
588
+ */
589
+ async areActiveForContext({ names, user, roles, groups, subjects, }) {
482
590
  let flags;
483
591
  let requestedNames;
484
592
  if (names === undefined) {
@@ -493,6 +601,7 @@ export class FeatureFlagModel {
493
601
  }
494
602
  const flagMap = new Map(flags.map((flag) => [flag.name, flag]));
495
603
  const result = {};
604
+ const subjectMatchedFlagIds = await this.getSubjectMatchedFlagIds(subjects);
496
605
  for (const name of requestedNames) {
497
606
  const flag = flagMap.get(name);
498
607
  if (!flag) {
@@ -513,10 +622,33 @@ export class FeatureFlagModel {
513
622
  user,
514
623
  roles,
515
624
  groups,
625
+ subjects,
626
+ subjectMatchedFlagIds,
516
627
  });
517
628
  }
518
629
  return result;
519
630
  }
631
+ /**
632
+ * Gets the latest modified timestamp across all feature flags.
633
+ *
634
+ * @returns The latest modified timestamp in milliseconds since epoch, or -1 if the table is empty
635
+ *
636
+ * @example
637
+ * ```typescript
638
+ * const lastModified = await model.getLastModified();
639
+ * if (lastModified === -1) {
640
+ * console.log("No feature flags exist");
641
+ * } else {
642
+ * console.log(`Last modified: ${new Date(lastModified)}`);
643
+ * }
644
+ * ```
645
+ */
646
+ async getLastModified() {
647
+ const sql = `SELECT MAX(modified) as max_modified FROM ${TABLE}`;
648
+ const res = await this.db.query(sql);
649
+ const maxModified = res.rows[0]?.max_modified;
650
+ return maxModified ? new Date(maxModified).getTime() : -1;
651
+ }
520
652
  }
521
653
  const GROUP_TABLE = "flapjack.feature_flag_group";
522
654
  const GROUP_MEMBER_TABLE = "flapjack.feature_flag_group_member";
@@ -789,6 +921,56 @@ export class FeatureFlagGroupModel {
789
921
  const res = await this.db.query(sql, [featureFlagId]);
790
922
  return res.rows.map(mapGroupRow);
791
923
  }
924
+ /**
925
+ * Adds an external subject identifier to a feature flag group.
926
+ */
927
+ async addSubject(groupId, subject) {
928
+ try {
929
+ const sql = `INSERT INTO ${GROUP_SUBJECT_TABLE} (feature_flag_group_id, subject)
930
+ VALUES ($1, $2)`;
931
+ await this.db.query(sql, [groupId, subject]);
932
+ return true;
933
+ }
934
+ catch (err) {
935
+ if (err.code === "23505") {
936
+ return false;
937
+ }
938
+ throw err;
939
+ }
940
+ }
941
+ /**
942
+ * Removes an external subject identifier from a feature flag group.
943
+ */
944
+ async removeSubject(groupId, subject) {
945
+ const sql = `DELETE FROM ${GROUP_SUBJECT_TABLE}
946
+ WHERE feature_flag_group_id = $1 AND subject = $2`;
947
+ const res = await this.db.query(sql, [groupId, subject]);
948
+ return res.rowCount > 0;
949
+ }
950
+ /**
951
+ * Gets all external subject identifiers associated with a feature flag group.
952
+ */
953
+ async getSubjects(groupId) {
954
+ const sql = `SELECT subject FROM ${GROUP_SUBJECT_TABLE}
955
+ WHERE feature_flag_group_id = $1
956
+ ORDER BY created DESC`;
957
+ const res = await this.db.query(sql, [groupId]);
958
+ return res.rows.map((row) => row.subject);
959
+ }
960
+ /**
961
+ * Gets all groups that are associated with a specific external subject identifier.
962
+ */
963
+ async getGroupsForSubject(subject) {
964
+ const sql = `
965
+ SELECT ${GROUP_COLUMNS.map((c) => `g.${c}`).join(", ")}
966
+ FROM ${GROUP_TABLE} g
967
+ INNER JOIN ${GROUP_SUBJECT_TABLE} gs ON g.id = gs.feature_flag_group_id
968
+ WHERE gs.subject = $1
969
+ ORDER BY g.created DESC
970
+ `;
971
+ const res = await this.db.query(sql, [subject]);
972
+ return res.rows.map(mapGroupRow);
973
+ }
792
974
  /**
793
975
  * Updates all feature flags in a group with the same changes.
794
976
  *
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @type {import('node-pg-migrate').ColumnDefinitions | undefined}
3
+ */
4
+ export const shorthands = undefined;
5
+
6
+ /**
7
+ * @param pgm {import('node-pg-migrate').MigrationBuilder}
8
+ * @param run {() => void | undefined}
9
+ * @returns {Promise<void> | void}
10
+ */
11
+ export const up = (pgm) => {
12
+ pgm.createTable(
13
+ { schema: "flapjack", name: "feature_flag_subject" },
14
+ {
15
+ id: "id",
16
+ feature_flag_id: {
17
+ type: "integer",
18
+ notNull: true,
19
+ references: { schema: "flapjack", name: "feature_flag" },
20
+ onDelete: "CASCADE",
21
+ },
22
+ subject: { type: "text", notNull: true },
23
+ created: {
24
+ type: "timestamptz",
25
+ notNull: true,
26
+ default: pgm.func("now()"),
27
+ },
28
+ },
29
+ );
30
+
31
+ pgm.addConstraint(
32
+ { schema: "flapjack", name: "feature_flag_subject" },
33
+ "unique_feature_flag_subject",
34
+ {
35
+ unique: ["feature_flag_id", "subject"],
36
+ },
37
+ );
38
+
39
+ pgm.createIndex(
40
+ { schema: "flapjack", name: "feature_flag_subject" },
41
+ "feature_flag_id",
42
+ );
43
+ pgm.createIndex(
44
+ { schema: "flapjack", name: "feature_flag_subject" },
45
+ "subject",
46
+ );
47
+
48
+ pgm.createTable(
49
+ { schema: "flapjack", name: "feature_flag_group_subject" },
50
+ {
51
+ id: "id",
52
+ feature_flag_group_id: {
53
+ type: "integer",
54
+ notNull: true,
55
+ references: { schema: "flapjack", name: "feature_flag_group" },
56
+ onDelete: "CASCADE",
57
+ },
58
+ subject: { type: "text", notNull: true },
59
+ created: {
60
+ type: "timestamptz",
61
+ notNull: true,
62
+ default: pgm.func("now()"),
63
+ },
64
+ },
65
+ );
66
+
67
+ pgm.addConstraint(
68
+ { schema: "flapjack", name: "feature_flag_group_subject" },
69
+ "unique_feature_flag_group_subject",
70
+ {
71
+ unique: ["feature_flag_group_id", "subject"],
72
+ },
73
+ );
74
+
75
+ pgm.createIndex(
76
+ { schema: "flapjack", name: "feature_flag_group_subject" },
77
+ "feature_flag_group_id",
78
+ );
79
+ pgm.createIndex(
80
+ { schema: "flapjack", name: "feature_flag_group_subject" },
81
+ "subject",
82
+ );
83
+ };
84
+
85
+ /**
86
+ * @param pgm {import('node-pg-migrate').MigrationBuilder}
87
+ * @param run {() => void | undefined}
88
+ * @returns {Promise<void> | void}
89
+ */
90
+ export const down = (pgm) => {
91
+ pgm.dropTable({ schema: "flapjack", name: "feature_flag_group_subject" });
92
+ pgm.dropTable({ schema: "flapjack", name: "feature_flag_subject" });
93
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandtg/flapjack",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "A simple feature flags library with PostgreSQL integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "clean": "rm -rf dist",
28
28
  "dev": "npx tsc --watch",
29
29
  "dev:env": "bash bin/dev/env.sh",
30
- "dev:migrate": "npx dotenv-cli -e env -- node-pg-migrate up",
30
+ "dev:migrate": "npx dotenv-cli -e .env -- node-pg-migrate up -s flapjack --create-schema -t pgmigrations",
31
31
  "dev:docker:up": "docker compose -f docker-compose.yml up -d",
32
32
  "dev:docker:down": "docker compose -f docker-compose.yml down -v",
33
33
  "create-migration": "npx node-pg-migrate create --",