@brandtg/flapjack 1.4.0 → 1.5.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
@@ -181,6 +181,36 @@ await featureFlags.isActiveForUser({
181
181
  });
182
182
  ```
183
183
 
184
+ ## Organizing Flags with Tags
185
+
186
+ Tags are freeform labels for organizing flags without encoding that information in
187
+ the flag name. They do **not** affect evaluation — they are purely organizational
188
+ metadata. A common use is associating a release version with a flag, which can then
189
+ be bumped via a database update without any code change.
190
+
191
+ ```typescript
192
+ // Tag a flag with a release version (and any other labels)
193
+ await featureFlags.create({
194
+ name: "checkout_v2",
195
+ tags: ["release:1.5.0", "checkout"],
196
+ });
197
+
198
+ // Find every flag shipping in a given release
199
+ const flags = await featureFlags.listByTag("release:1.5.0");
200
+
201
+ // Bump the release version later — no code change required
202
+ await featureFlags.update(flag.id, { tags: ["release:1.6.0", "checkout"] });
203
+ ```
204
+
205
+ The equivalent CLI commands:
206
+
207
+ ```bash
208
+ flapjack create --name checkout_v2 --tags release:1.5.0 checkout
209
+ flapjack list-by-tag release:1.5.0
210
+ flapjack update <id> --tags release:1.6.0 checkout
211
+ flapjack update <id> --clear-tags
212
+ ```
213
+
184
214
  ## Performance Considerations
185
215
 
186
216
  ⚠️ **Important**: Without caching, Flapjack queries the database on every `isActiveForUser()` call. For high-traffic applications, use the built-in caching layer:
@@ -342,17 +372,21 @@ await featureFlags.update(flagId, { percent: 100 });
342
372
 
343
373
  Creates a new feature flag.
344
374
 
345
- #### `getById(id: number): Promise<FeatureFlag | null>`
375
+ #### `getById(id: number, options?: { includeArchived?: boolean }): Promise<FeatureFlag | null>`
376
+
377
+ Retrieves a feature flag by its ID. Archived flags are excluded unless `includeArchived` is `true`.
346
378
 
347
- Retrieves a feature flag by its ID.
379
+ #### `getByName(name: string, options?: { includeArchived?: boolean }): Promise<FeatureFlag | null>`
348
380
 
349
- #### `getByName(name: string): Promise<FeatureFlag | null>`
381
+ Retrieves a feature flag by its name. Archived flags are excluded unless `includeArchived` is `true`.
350
382
 
351
- Retrieves a feature flag by its name.
383
+ #### `list(options?: { includeArchived?: boolean }): Promise<FeatureFlag[]>`
352
384
 
353
- #### `list(): Promise<FeatureFlag[]>`
385
+ Lists all feature flags, ordered by ID. Archived flags are excluded unless `includeArchived` is `true`.
354
386
 
355
- Lists all feature flags, ordered by ID.
387
+ #### `listByTag(tag: string, options?: { includeArchived?: boolean }): Promise<FeatureFlag[]>`
388
+
389
+ Lists all feature flags carrying the given tag (exact match), ordered by ID. Archived flags are excluded unless `includeArchived` is `true`.
356
390
 
357
391
  #### `update(id: number, changes: UpdateChanges): Promise<FeatureFlag | null>`
358
392
 
@@ -360,7 +394,13 @@ Updates a feature flag. Returns the updated flag or null if not found.
360
394
 
361
395
  #### `delete(id: number): Promise<boolean>`
362
396
 
363
- Deletes a feature flag. Returns true if deleted, false if not found.
397
+ Permanently deletes a feature flag and all its relationships. Returns true if deleted, false if not found. To hide a flag while keeping its history, use `archive` instead.
398
+
399
+ #### `archive(id: number): Promise<FeatureFlag | null>`
400
+
401
+ Archives (hides) a feature flag while keeping the row for historical/audit purposes. Archived flags are excluded from normal reads and evaluate as inactive, but remain retrievable via `{ includeArchived: true }`. Returns the archived flag, or null if it does not exist or is already archived.
402
+
403
+ Archiving is **irreversible** (there is no `unarchive`) and the `archived` timestamp is immutable once set. The flag's name stays reserved permanently — a new flag cannot reuse it; create a new flag with a different name instead. The same `archive(id)` / `includeArchived` semantics apply to `FeatureFlagGroupModel`.
364
404
 
365
405
  #### `isActiveForUser(params): Promise<boolean>`
366
406
 
@@ -384,6 +424,13 @@ flapjack create --name my_feature --roles admin --note "Admin-only feature"
384
424
  # List all flags
385
425
  flapjack list
386
426
 
427
+ # Include archived (hidden) flags in any read command
428
+ flapjack list --include-archived
429
+ flapjack get-by-name my_feature --include-archived
430
+
431
+ # List flags carrying a tag (e.g. a release version)
432
+ flapjack list-by-tag release:1.5.0
433
+
387
434
  # Get a specific flag
388
435
  flapjack get-by-name my_feature
389
436
 
@@ -405,9 +452,12 @@ flapjack update 1 --percent 50 --everyone false
405
452
  # Clear specific fields
406
453
  flapjack update 1 --clear-roles --clear-percent
407
454
 
408
- # Delete a flag
455
+ # Delete a flag (permanent — destroys all metadata)
409
456
  flapjack delete 1
410
457
 
458
+ # Archive a flag (hide it but keep its history; irreversible)
459
+ flapjack archive 1
460
+
411
461
  # Add/remove/list subject mappings on a flag
412
462
  flapjack add-subject 1 tenant:acme
413
463
  flapjack remove-subject 1 tenant:acme
@@ -421,6 +471,7 @@ flapjack group-get 1
421
471
  flapjack group-get-by-name checkout_rollout
422
472
  flapjack group-update 1 --note "Updated"
423
473
  flapjack group-delete 1
474
+ flapjack group-archive 1
424
475
 
425
476
  # Group membership
426
477
  flapjack group-add-flag 1 10
@@ -490,8 +541,11 @@ await featureFlags.update(flagId, { percent: 10 });
490
541
  await featureFlags.update(flagId, { everyone: true });
491
542
 
492
543
  // 5. Cleanup (after feature is stable)
493
- // Remove feature flag checks from code
494
- await featureFlags.delete(flagId);
544
+ // Remove feature flag checks from code, then either:
545
+ // - delete to remove it entirely, or
546
+ // - archive to hide it while keeping its history for audit
547
+ await featureFlags.archive(flagId);
548
+ // (archive is irreversible and the name stays reserved permanently)
495
549
  ```
496
550
 
497
551
  ### Error Handling
package/dist/cli.js CHANGED
@@ -47,6 +47,10 @@ const flagOptions = {
47
47
  type: "array",
48
48
  describe: "List of specific user IDs that have this flag enabled",
49
49
  },
50
+ tags: {
51
+ type: "array",
52
+ describe: "List of tags for organizing this flag (e.g. release:1.5.0)",
53
+ },
50
54
  note: {
51
55
  type: "string",
52
56
  describe: "Description of where this flag is used and what it does",
@@ -56,6 +60,14 @@ const flagOptions = {
56
60
  describe: "Expiration date (ISO 8601 format)",
57
61
  },
58
62
  };
63
+ // Option to include archived (hidden) records in read commands
64
+ const includeArchivedOption = {
65
+ "include-archived": {
66
+ type: "boolean",
67
+ default: false,
68
+ describe: "Include archived (hidden) records in the results",
69
+ },
70
+ };
59
71
  // Create command
60
72
  cli.command("create", "Create a new feature flag", (yargs) => {
61
73
  return yargs
@@ -78,6 +90,8 @@ cli.command("create", "Create a new feature flag", (yargs) => {
78
90
  input.groups = argv.groups;
79
91
  if (argv.users !== undefined)
80
92
  input.users = argv.users;
93
+ if (argv.tags !== undefined)
94
+ input.tags = argv.tags;
81
95
  if (argv.note !== undefined)
82
96
  input.note = argv.note;
83
97
  if (argv.expires !== undefined)
@@ -95,15 +109,19 @@ cli.command("create", "Create a new feature flag", (yargs) => {
95
109
  });
96
110
  // Get by ID command
97
111
  cli.command("get <id>", "Get a feature flag by ID", (yargs) => {
98
- return yargs.positional("id", {
112
+ return yargs
113
+ .positional("id", {
99
114
  type: "number",
100
115
  describe: "Feature flag ID",
101
- });
116
+ })
117
+ .options(includeArchivedOption);
102
118
  }, async (argv) => {
103
119
  const db = createDatabase();
104
120
  const model = new FeatureFlagModel(db);
105
121
  try {
106
- const flag = await model.getById(argv.id);
122
+ const flag = await model.getById(argv.id, {
123
+ includeArchived: argv.includeArchived,
124
+ });
107
125
  if (flag) {
108
126
  console.log(JSON.stringify(flag, null, 2));
109
127
  }
@@ -122,15 +140,19 @@ cli.command("get <id>", "Get a feature flag by ID", (yargs) => {
122
140
  });
123
141
  // Get by name command
124
142
  cli.command("get-by-name <name>", "Get a feature flag by name", (yargs) => {
125
- return yargs.positional("name", {
143
+ return yargs
144
+ .positional("name", {
126
145
  type: "string",
127
146
  describe: "Feature flag name",
128
- });
147
+ })
148
+ .options(includeArchivedOption);
129
149
  }, async (argv) => {
130
150
  const db = createDatabase();
131
151
  const model = new FeatureFlagModel(db);
132
152
  try {
133
- const flag = await model.getByName(argv.name);
153
+ const flag = await model.getByName(argv.name, {
154
+ includeArchived: argv.includeArchived,
155
+ });
134
156
  if (flag) {
135
157
  console.log(JSON.stringify(flag, null, 2));
136
158
  }
@@ -148,11 +170,15 @@ cli.command("get-by-name <name>", "Get a feature flag by name", (yargs) => {
148
170
  }
149
171
  });
150
172
  // List command
151
- cli.command("list", "List all feature flags", () => { }, async () => {
173
+ cli.command("list", "List all feature flags", (yargs) => {
174
+ return yargs.options(includeArchivedOption);
175
+ }, async (argv) => {
152
176
  const db = createDatabase();
153
177
  const model = new FeatureFlagModel(db);
154
178
  try {
155
- const flags = await model.list();
179
+ const flags = await model.list({
180
+ includeArchived: argv.includeArchived,
181
+ });
156
182
  console.log(JSON.stringify(flags, null, 2));
157
183
  }
158
184
  catch (error) {
@@ -163,6 +189,31 @@ cli.command("list", "List all feature flags", () => { }, async () => {
163
189
  await db.end();
164
190
  }
165
191
  });
192
+ // List flags carrying a given tag
193
+ cli.command("list-by-tag <tag>", "List feature flags carrying a given tag (e.g. release:1.5.0)", (yargs) => {
194
+ return yargs
195
+ .positional("tag", {
196
+ type: "string",
197
+ describe: "Tag to filter by",
198
+ })
199
+ .options(includeArchivedOption);
200
+ }, async (argv) => {
201
+ const db = createDatabase();
202
+ const model = new FeatureFlagModel(db);
203
+ try {
204
+ const flags = await model.listByTag(argv.tag, {
205
+ includeArchived: argv.includeArchived,
206
+ });
207
+ console.log(JSON.stringify(flags, null, 2));
208
+ }
209
+ catch (error) {
210
+ console.error("Error listing feature flags by tag:", error);
211
+ process.exit(1);
212
+ }
213
+ finally {
214
+ await db.end();
215
+ }
216
+ });
166
217
  // Update command
167
218
  cli.command("update <id>", "Update a feature flag", (yargs) => {
168
219
  return yargs
@@ -192,6 +243,10 @@ cli.command("update <id>", "Update a feature flag", (yargs) => {
192
243
  type: "boolean",
193
244
  describe: "Clear users list",
194
245
  },
246
+ "clear-tags": {
247
+ type: "boolean",
248
+ describe: "Clear tags list",
249
+ },
195
250
  "clear-note": {
196
251
  type: "boolean",
197
252
  describe: "Clear the note",
@@ -233,6 +288,11 @@ cli.command("update <id>", "Update a feature flag", (yargs) => {
233
288
  changes.users = null;
234
289
  else if (argv.users !== undefined)
235
290
  changes.users = argv.users;
291
+ // Tags: clear flag takes precedence
292
+ if (argv.clearTags)
293
+ changes.tags = null;
294
+ else if (argv.tags !== undefined)
295
+ changes.tags = argv.tags;
236
296
  // Note: clear flag takes precedence
237
297
  if (argv.clearNote)
238
298
  changes.note = null;
@@ -287,6 +347,33 @@ cli.command("delete <id>", "Delete a feature flag", (yargs) => {
287
347
  await db.end();
288
348
  }
289
349
  });
350
+ // Archive command (soft delete; irreversible, hides the flag but keeps history)
351
+ cli.command("archive <id>", "Archive (hide) a feature flag while keeping its history. Irreversible.", (yargs) => {
352
+ return yargs.positional("id", {
353
+ type: "number",
354
+ describe: "Feature flag ID",
355
+ });
356
+ }, async (argv) => {
357
+ const db = createDatabase();
358
+ const model = new FeatureFlagModel(db);
359
+ try {
360
+ const flag = await model.archive(argv.id);
361
+ if (flag) {
362
+ console.log(JSON.stringify(flag, null, 2));
363
+ }
364
+ else {
365
+ console.log(`Feature flag with ID ${argv.id} not found or already archived`);
366
+ process.exit(1);
367
+ }
368
+ }
369
+ catch (error) {
370
+ console.error("Error archiving feature flag:", error);
371
+ process.exit(1);
372
+ }
373
+ finally {
374
+ await db.end();
375
+ }
376
+ });
290
377
  // Check if active for user command
291
378
  cli.command("is-active <name>", "Check if a feature flag is active for a user", (yargs) => {
292
379
  return yargs
@@ -626,11 +713,15 @@ cli.command("group-create", "Create a feature flag group", (yargs) => {
626
713
  }
627
714
  });
628
715
  // List feature flag groups command
629
- cli.command("group-list", "List all feature flag groups", () => { }, async () => {
716
+ cli.command("group-list", "List all feature flag groups", (yargs) => {
717
+ return yargs.options(includeArchivedOption);
718
+ }, async (argv) => {
630
719
  const db = createDatabase();
631
720
  const model = new FeatureFlagGroupModel(db);
632
721
  try {
633
- const groups = await model.list();
722
+ const groups = await model.list({
723
+ includeArchived: argv.includeArchived,
724
+ });
634
725
  console.log(JSON.stringify(groups, null, 2));
635
726
  }
636
727
  catch (error) {
@@ -643,15 +734,19 @@ cli.command("group-list", "List all feature flag groups", () => { }, async () =>
643
734
  });
644
735
  // Get feature flag group by ID command
645
736
  cli.command("group-get <id>", "Get a feature flag group by ID", (yargs) => {
646
- return yargs.positional("id", {
737
+ return yargs
738
+ .positional("id", {
647
739
  type: "number",
648
740
  describe: "Feature flag group ID",
649
- });
741
+ })
742
+ .options(includeArchivedOption);
650
743
  }, async (argv) => {
651
744
  const db = createDatabase();
652
745
  const model = new FeatureFlagGroupModel(db);
653
746
  try {
654
- const group = await model.getById(argv.id);
747
+ const group = await model.getById(argv.id, {
748
+ includeArchived: argv.includeArchived,
749
+ });
655
750
  if (!group) {
656
751
  console.log(`Feature flag group with ID ${argv.id} not found`);
657
752
  process.exit(1);
@@ -668,15 +763,19 @@ cli.command("group-get <id>", "Get a feature flag group by ID", (yargs) => {
668
763
  });
669
764
  // Get feature flag group by name command
670
765
  cli.command("group-get-by-name <name>", "Get a feature flag group by name", (yargs) => {
671
- return yargs.positional("name", {
766
+ return yargs
767
+ .positional("name", {
672
768
  type: "string",
673
769
  describe: "Feature flag group name",
674
- });
770
+ })
771
+ .options(includeArchivedOption);
675
772
  }, async (argv) => {
676
773
  const db = createDatabase();
677
774
  const model = new FeatureFlagGroupModel(db);
678
775
  try {
679
- const group = await model.getByName(argv.name);
776
+ const group = await model.getByName(argv.name, {
777
+ includeArchived: argv.includeArchived,
778
+ });
680
779
  if (!group) {
681
780
  console.log(`Feature flag group with name "${argv.name}" not found`);
682
781
  process.exit(1);
@@ -763,6 +862,31 @@ cli.command("group-delete <id>", "Delete a feature flag group", (yargs) => {
763
862
  await db.end();
764
863
  }
765
864
  });
865
+ // Archive a feature flag group (soft delete; irreversible, keeps history)
866
+ cli.command("group-archive <id>", "Archive (hide) a feature flag group while keeping its history. Irreversible.", (yargs) => {
867
+ return yargs.positional("id", {
868
+ type: "number",
869
+ describe: "Feature flag group ID",
870
+ });
871
+ }, async (argv) => {
872
+ const db = createDatabase();
873
+ const model = new FeatureFlagGroupModel(db);
874
+ try {
875
+ const group = await model.archive(argv.id);
876
+ if (!group) {
877
+ console.log(`Feature flag group with ID ${argv.id} not found or already archived`);
878
+ process.exit(1);
879
+ }
880
+ console.log(JSON.stringify(group, null, 2));
881
+ }
882
+ catch (error) {
883
+ console.error("Error archiving feature flag group:", error);
884
+ process.exit(1);
885
+ }
886
+ finally {
887
+ await db.end();
888
+ }
889
+ });
766
890
  // Add flag to feature flag group command
767
891
  cli.command("group-add-flag <groupId> <flagId>", "Add a feature flag to a group", (yargs) => {
768
892
  return yargs
package/dist/model.d.ts CHANGED
@@ -3,8 +3,8 @@ import type { QueryResult } from "pg";
3
3
  interface Queryable {
4
4
  query: (text: string, params?: any[]) => Promise<QueryResult>;
5
5
  }
6
- type CreateInput = Omit<FeatureFlag, "id" | "created" | "modified">;
7
- type UpdateChanges = Partial<Omit<FeatureFlag, "id" | "created" | "modified">>;
6
+ type CreateInput = Omit<FeatureFlag, "id" | "created" | "modified" | "archived">;
7
+ type UpdateChanges = Partial<Omit<FeatureFlag, "id" | "created" | "modified" | "archived">>;
8
8
  /**
9
9
  * Model for managing feature flags stored in PostgreSQL.
10
10
  *
@@ -43,6 +43,7 @@ export declare class FeatureFlagModel {
43
43
  * @param input.roles - Optional list of roles that have this flag enabled
44
44
  * @param input.groups - Optional list of user groups that have this flag enabled
45
45
  * @param input.users - Optional list of specific user IDs that have this flag enabled
46
+ * @param input.tags - Optional list of tags for organizing the flag (e.g. a release version)
46
47
  * @param input.note - Optional description of the flag's purpose
47
48
  * @param input.expires - Optional expiration date for the feature flag
48
49
  * @returns The created feature flag with generated id, created, and modified timestamps
@@ -73,7 +74,9 @@ export declare class FeatureFlagModel {
73
74
  * }
74
75
  * ```
75
76
  */
76
- getById(id: number): Promise<FeatureFlag | null>;
77
+ getById(id: number, { includeArchived }?: {
78
+ includeArchived?: boolean;
79
+ }): Promise<FeatureFlag | null>;
77
80
  /**
78
81
  * Retrieves a feature flag by its name.
79
82
  *
@@ -88,7 +91,9 @@ export declare class FeatureFlagModel {
88
91
  * }
89
92
  * ```
90
93
  */
91
- getByName(name: string): Promise<FeatureFlag | null>;
94
+ getByName(name: string, { includeArchived }?: {
95
+ includeArchived?: boolean;
96
+ }): Promise<FeatureFlag | null>;
92
97
  /**
93
98
  * Retrieves multiple feature flags by their names.
94
99
  *
@@ -101,7 +106,9 @@ export declare class FeatureFlagModel {
101
106
  * console.log(`Found ${flags.length} flags`);
102
107
  * ```
103
108
  */
104
- getManyByName(names: string[]): Promise<FeatureFlag[]>;
109
+ getManyByName(names: string[], { includeArchived }?: {
110
+ includeArchived?: boolean;
111
+ }): Promise<FeatureFlag[]>;
105
112
  /**
106
113
  * Retrieves multiple feature flags by their IDs.
107
114
  *
@@ -114,7 +121,9 @@ export declare class FeatureFlagModel {
114
121
  * console.log(`Found ${flags.length} flags`);
115
122
  * ```
116
123
  */
117
- getMany(ids: number[]): Promise<FeatureFlag[]>;
124
+ getMany(ids: number[], { includeArchived }?: {
125
+ includeArchived?: boolean;
126
+ }): Promise<FeatureFlag[]>;
118
127
  /**
119
128
  * Retrieves all feature flags, ordered by ID.
120
129
  *
@@ -129,7 +138,24 @@ export declare class FeatureFlagModel {
129
138
  * });
130
139
  * ```
131
140
  */
132
- list(): Promise<FeatureFlag[]>;
141
+ list({ includeArchived, }?: {
142
+ includeArchived?: boolean;
143
+ }): Promise<FeatureFlag[]>;
144
+ /**
145
+ * Retrieves all feature flags that carry the given tag.
146
+ *
147
+ * @param tag - The tag to filter by (exact match against a tag in the flag's tags array)
148
+ * @returns Array of feature flags tagged with the given value
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * // Find every flag associated with a release version
153
+ * const flags = await model.listByTag("release:1.5.0");
154
+ * ```
155
+ */
156
+ listByTag(tag: string, { includeArchived }?: {
157
+ includeArchived?: boolean;
158
+ }): Promise<FeatureFlag[]>;
133
159
  /**
134
160
  * Updates an existing feature flag.
135
161
  *
@@ -156,11 +182,16 @@ export declare class FeatureFlagModel {
156
182
  */
157
183
  update(id: number, changes: UpdateChanges): Promise<FeatureFlag | null>;
158
184
  /**
159
- * Deletes a feature flag from the database.
185
+ * Permanently deletes a feature flag from the database, including its
186
+ * subject and group-member relationships (via ON DELETE CASCADE).
160
187
  *
161
188
  * @param id - The unique identifier of the feature flag to delete
162
189
  * @returns true if the flag was deleted, false if not found
163
190
  *
191
+ * @remarks
192
+ * This is a hard delete that destroys all metadata. To hide a flag while
193
+ * retaining its history for audit purposes, use {@link archive} instead.
194
+ *
164
195
  * @example
165
196
  * ```typescript
166
197
  * const deleted = await model.delete(flagId);
@@ -170,6 +201,30 @@ export declare class FeatureFlagModel {
170
201
  * ```
171
202
  */
172
203
  delete(id: number): Promise<boolean>;
204
+ /**
205
+ * Archives (hides) a feature flag while keeping the row for historical/audit
206
+ * purposes. Archived flags are excluded from normal reads and evaluation
207
+ * (an archived flag evaluates to inactive), but can still be retrieved by
208
+ * passing `{ includeArchived: true }` to the read methods.
209
+ *
210
+ * @param id - The unique identifier of the feature flag to archive
211
+ * @returns The archived feature flag, or null if it does not exist or is already archived
212
+ *
213
+ * @remarks
214
+ * Archiving is irreversible: there is intentionally no `unarchive` method.
215
+ * The `archived` timestamp is set once and never changed (a second call is a
216
+ * no-op that returns null). The flag's name remains reserved permanently, so
217
+ * a new flag cannot reuse it — create a new flag with a different name.
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * const archived = await model.archive(flagId);
222
+ * if (archived) {
223
+ * console.log(`Archived at ${archived.archived}`);
224
+ * }
225
+ * ```
226
+ */
227
+ archive(id: number): Promise<FeatureFlag | null>;
173
228
  /**
174
229
  * Adds an external subject identifier to a feature flag.
175
230
  */
@@ -185,7 +240,9 @@ export declare class FeatureFlagModel {
185
240
  /**
186
241
  * Gets all feature flags directly associated with an external subject identifier.
187
242
  */
188
- getFeatureFlagsForSubject(subject: string): Promise<FeatureFlag[]>;
243
+ getFeatureFlagsForSubject(subject: string, { includeArchived }?: {
244
+ includeArchived?: boolean;
245
+ }): Promise<FeatureFlag[]>;
189
246
  private getSubjectMatchedFlagIds;
190
247
  /**
191
248
  * Checks if a user belongs to any of the specified groups
@@ -354,8 +411,8 @@ export declare class FeatureFlagModel {
354
411
  */
355
412
  getLastModified(): Promise<number>;
356
413
  }
357
- type CreateGroupInput = Omit<FeatureFlagGroup, "id" | "created" | "modified">;
358
- type UpdateGroupChanges = Partial<Omit<FeatureFlagGroup, "id" | "created" | "modified">>;
414
+ type CreateGroupInput = Omit<FeatureFlagGroup, "id" | "created" | "modified" | "archived">;
415
+ type UpdateGroupChanges = Partial<Omit<FeatureFlagGroup, "id" | "created" | "modified" | "archived">>;
359
416
  type UpdateAllChanges = Partial<Pick<FeatureFlag, "everyone" | "percent" | "roles" | "groups" | "users">>;
360
417
  /**
361
418
  * Model for managing feature flag groups stored in PostgreSQL.
@@ -416,7 +473,9 @@ export declare class FeatureFlagGroupModel {
416
473
  * }
417
474
  * ```
418
475
  */
419
- getById(id: number): Promise<FeatureFlagGroup | null>;
476
+ getById(id: number, { includeArchived }?: {
477
+ includeArchived?: boolean;
478
+ }): Promise<FeatureFlagGroup | null>;
420
479
  /**
421
480
  * Retrieves a feature flag group by its name.
422
481
  *
@@ -428,7 +487,9 @@ export declare class FeatureFlagGroupModel {
428
487
  * const group = await model.getByName("billing_redesign");
429
488
  * ```
430
489
  */
431
- getByName(name: string): Promise<FeatureFlagGroup | null>;
490
+ getByName(name: string, { includeArchived }?: {
491
+ includeArchived?: boolean;
492
+ }): Promise<FeatureFlagGroup | null>;
432
493
  /**
433
494
  * Lists all feature flag groups.
434
495
  *
@@ -442,7 +503,9 @@ export declare class FeatureFlagGroupModel {
442
503
  * }
443
504
  * ```
444
505
  */
445
- list(): Promise<FeatureFlagGroup[]>;
506
+ list({ includeArchived, }?: {
507
+ includeArchived?: boolean;
508
+ }): Promise<FeatureFlagGroup[]>;
446
509
  /**
447
510
  * Updates a feature flag group.
448
511
  *
@@ -459,17 +522,43 @@ export declare class FeatureFlagGroupModel {
459
522
  */
460
523
  update(id: number, changes: UpdateGroupChanges): Promise<FeatureFlagGroup | null>;
461
524
  /**
462
- * Deletes a feature flag group and all its member relationships.
525
+ * Permanently deletes a feature flag group and all its member and subject
526
+ * relationships (via ON DELETE CASCADE).
463
527
  *
464
528
  * @param id - The group ID to delete
465
529
  * @returns true if deleted, false if not found
466
530
  *
531
+ * @remarks
532
+ * This is a hard delete that destroys all metadata. To hide a group while
533
+ * retaining its history for audit purposes, use {@link archive} instead.
534
+ *
467
535
  * @example
468
536
  * ```typescript
469
537
  * const deleted = await model.delete(1);
470
538
  * ```
471
539
  */
472
540
  delete(id: number): Promise<boolean>;
541
+ /**
542
+ * Archives (hides) a feature flag group while keeping the row for
543
+ * historical/audit purposes. Archived groups are excluded from normal reads
544
+ * and no longer contribute subject-based targeting. Member flags are left
545
+ * untouched and remain active; the membership and subject rows are kept for
546
+ * audit but become inert.
547
+ *
548
+ * @param id - The group ID to archive
549
+ * @returns The archived group, or null if it does not exist or is already archived
550
+ *
551
+ * @remarks
552
+ * Archiving is irreversible: there is intentionally no `unarchive` method.
553
+ * The `archived` timestamp is set once and never changed (a second call is a
554
+ * no-op that returns null). The group's name remains reserved permanently.
555
+ *
556
+ * @example
557
+ * ```typescript
558
+ * const archived = await model.archive(1);
559
+ * ```
560
+ */
561
+ archive(id: number): Promise<FeatureFlagGroup | null>;
473
562
  /**
474
563
  * Adds a feature flag to a group.
475
564
  *
@@ -512,7 +601,9 @@ export declare class FeatureFlagGroupModel {
512
601
  * }
513
602
  * ```
514
603
  */
515
- getFeatureFlags(groupId: number): Promise<FeatureFlag[]>;
604
+ getFeatureFlags(groupId: number, { includeArchived }?: {
605
+ includeArchived?: boolean;
606
+ }): Promise<FeatureFlag[]>;
516
607
  /**
517
608
  * Gets all groups that contain a specific feature flag.
518
609
  *
@@ -524,7 +615,9 @@ export declare class FeatureFlagGroupModel {
524
615
  * const groups = await model.getGroupsForFeatureFlag(5);
525
616
  * ```
526
617
  */
527
- getGroupsForFeatureFlag(featureFlagId: number): Promise<FeatureFlagGroup[]>;
618
+ getGroupsForFeatureFlag(featureFlagId: number, { includeArchived }?: {
619
+ includeArchived?: boolean;
620
+ }): Promise<FeatureFlagGroup[]>;
528
621
  /**
529
622
  * Adds an external subject identifier to a feature flag group.
530
623
  */
@@ -540,7 +633,9 @@ export declare class FeatureFlagGroupModel {
540
633
  /**
541
634
  * Gets all groups that are associated with a specific external subject identifier.
542
635
  */
543
- getGroupsForSubject(subject: string): Promise<FeatureFlagGroup[]>;
636
+ getGroupsForSubject(subject: string, { includeArchived }?: {
637
+ includeArchived?: boolean;
638
+ }): Promise<FeatureFlagGroup[]>;
544
639
  /**
545
640
  * Updates all feature flags in a group with the same changes.
546
641
  *
@@ -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
@@ -10,10 +10,12 @@ const COLUMNS = [
10
10
  "roles",
11
11
  "groups",
12
12
  "users",
13
+ "tags",
13
14
  "note",
14
15
  "created",
15
16
  "modified",
16
17
  "expires",
18
+ "archived",
17
19
  ];
18
20
  function mapRow(row) {
19
21
  return {
@@ -28,10 +30,12 @@ function mapRow(row) {
28
30
  ? row.groups
29
31
  : undefined,
30
32
  users: row.users && row.users.length > 0 ? row.users : undefined,
33
+ tags: row.tags && row.tags.length > 0 ? row.tags : undefined,
31
34
  note: row.note ?? undefined,
32
35
  created: new Date(row.created),
33
36
  modified: new Date(row.modified),
34
37
  expires: row.expires ? new Date(row.expires) : undefined,
38
+ archived: row.archived ? new Date(row.archived) : undefined,
35
39
  };
36
40
  }
37
41
  /**
@@ -75,6 +79,7 @@ export class FeatureFlagModel {
75
79
  * @param input.roles - Optional list of roles that have this flag enabled
76
80
  * @param input.groups - Optional list of user groups that have this flag enabled
77
81
  * @param input.users - Optional list of specific user IDs that have this flag enabled
82
+ * @param input.tags - Optional list of tags for organizing the flag (e.g. a release version)
78
83
  * @param input.note - Optional description of the flag's purpose
79
84
  * @param input.expires - Optional expiration date for the feature flag
80
85
  * @returns The created feature flag with generated id, created, and modified timestamps
@@ -113,6 +118,10 @@ export class FeatureFlagModel {
113
118
  cols.push("users");
114
119
  vals.push(input.users ?? null);
115
120
  }
121
+ if ("tags" in input) {
122
+ cols.push("tags");
123
+ vals.push(input.tags ?? null);
124
+ }
116
125
  if ("note" in input) {
117
126
  cols.push("note");
118
127
  vals.push(input.note ?? null);
@@ -140,8 +149,9 @@ export class FeatureFlagModel {
140
149
  * }
141
150
  * ```
142
151
  */
143
- async getById(id) {
144
- const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE id = $1`;
152
+ async getById(id, { includeArchived = false } = {}) {
153
+ const filter = includeArchived ? "" : " AND archived IS NULL";
154
+ const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE id = $1${filter}`;
145
155
  const res = await this.db.query(sql, [id]);
146
156
  if (res.rows.length === 0)
147
157
  return null;
@@ -161,8 +171,9 @@ export class FeatureFlagModel {
161
171
  * }
162
172
  * ```
163
173
  */
164
- async getByName(name) {
165
- const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE name = $1`;
174
+ async getByName(name, { includeArchived = false } = {}) {
175
+ const filter = includeArchived ? "" : " AND archived IS NULL";
176
+ const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE name = $1${filter}`;
166
177
  const res = await this.db.query(sql, [name]);
167
178
  if (res.rows.length === 0)
168
179
  return null;
@@ -180,10 +191,11 @@ export class FeatureFlagModel {
180
191
  * console.log(`Found ${flags.length} flags`);
181
192
  * ```
182
193
  */
183
- async getManyByName(names) {
194
+ async getManyByName(names, { includeArchived = false } = {}) {
184
195
  if (names.length === 0)
185
196
  return [];
186
- const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE name = ANY($1)`;
197
+ const filter = includeArchived ? "" : " AND archived IS NULL";
198
+ const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE name = ANY($1)${filter}`;
187
199
  const res = await this.db.query(sql, [names]);
188
200
  return res.rows.map(mapRow);
189
201
  }
@@ -199,10 +211,11 @@ export class FeatureFlagModel {
199
211
  * console.log(`Found ${flags.length} flags`);
200
212
  * ```
201
213
  */
202
- async getMany(ids) {
214
+ async getMany(ids, { includeArchived = false } = {}) {
203
215
  if (ids.length === 0)
204
216
  return [];
205
- const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE id = ANY($1)`;
217
+ const filter = includeArchived ? "" : " AND archived IS NULL";
218
+ const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE id = ANY($1)${filter}`;
206
219
  const res = await this.db.query(sql, [ids]);
207
220
  return res.rows.map(mapRow);
208
221
  }
@@ -220,11 +233,30 @@ export class FeatureFlagModel {
220
233
  * });
221
234
  * ```
222
235
  */
223
- async list() {
224
- const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} ORDER BY id`;
236
+ async list({ includeArchived = false, } = {}) {
237
+ const filter = includeArchived ? "" : " WHERE archived IS NULL";
238
+ const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE}${filter} ORDER BY id`;
225
239
  const res = await this.db.query(sql);
226
240
  return res.rows.map(mapRow);
227
241
  }
242
+ /**
243
+ * Retrieves all feature flags that carry the given tag.
244
+ *
245
+ * @param tag - The tag to filter by (exact match against a tag in the flag's tags array)
246
+ * @returns Array of feature flags tagged with the given value
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * // Find every flag associated with a release version
251
+ * const flags = await model.listByTag("release:1.5.0");
252
+ * ```
253
+ */
254
+ async listByTag(tag, { includeArchived = false } = {}) {
255
+ const filter = includeArchived ? "" : " AND archived IS NULL";
256
+ const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE $1 = ANY(tags)${filter} ORDER BY id`;
257
+ const res = await this.db.query(sql, [tag]);
258
+ return res.rows.map(mapRow);
259
+ }
228
260
  /**
229
261
  * Updates an existing feature flag.
230
262
  *
@@ -276,6 +308,10 @@ export class FeatureFlagModel {
276
308
  sets.push(`users = $${sets.length + 1}`);
277
309
  vals.push(changes.users ?? null);
278
310
  }
311
+ if ("tags" in changes) {
312
+ sets.push(`tags = $${sets.length + 1}`);
313
+ vals.push(changes.tags ?? null);
314
+ }
279
315
  if ("note" in changes) {
280
316
  sets.push(`note = $${sets.length + 1}`);
281
317
  vals.push(changes.note ?? null);
@@ -296,11 +332,16 @@ export class FeatureFlagModel {
296
332
  return mapRow(res.rows[0]);
297
333
  }
298
334
  /**
299
- * Deletes a feature flag from the database.
335
+ * Permanently deletes a feature flag from the database, including its
336
+ * subject and group-member relationships (via ON DELETE CASCADE).
300
337
  *
301
338
  * @param id - The unique identifier of the feature flag to delete
302
339
  * @returns true if the flag was deleted, false if not found
303
340
  *
341
+ * @remarks
342
+ * This is a hard delete that destroys all metadata. To hide a flag while
343
+ * retaining its history for audit purposes, use {@link archive} instead.
344
+ *
304
345
  * @example
305
346
  * ```typescript
306
347
  * const deleted = await model.delete(flagId);
@@ -313,6 +354,36 @@ export class FeatureFlagModel {
313
354
  const res = await this.db.query(`DELETE FROM ${TABLE} WHERE id = $1`, [id]);
314
355
  return res.rowCount > 0;
315
356
  }
357
+ /**
358
+ * Archives (hides) a feature flag while keeping the row for historical/audit
359
+ * purposes. Archived flags are excluded from normal reads and evaluation
360
+ * (an archived flag evaluates to inactive), but can still be retrieved by
361
+ * passing `{ includeArchived: true }` to the read methods.
362
+ *
363
+ * @param id - The unique identifier of the feature flag to archive
364
+ * @returns The archived feature flag, or null if it does not exist or is already archived
365
+ *
366
+ * @remarks
367
+ * Archiving is irreversible: there is intentionally no `unarchive` method.
368
+ * The `archived` timestamp is set once and never changed (a second call is a
369
+ * no-op that returns null). The flag's name remains reserved permanently, so
370
+ * a new flag cannot reuse it — create a new flag with a different name.
371
+ *
372
+ * @example
373
+ * ```typescript
374
+ * const archived = await model.archive(flagId);
375
+ * if (archived) {
376
+ * console.log(`Archived at ${archived.archived}`);
377
+ * }
378
+ * ```
379
+ */
380
+ async archive(id) {
381
+ const sql = `UPDATE ${TABLE} SET archived = now() WHERE id = $1 AND archived IS NULL RETURNING ${COLUMNS.join(", ")}`;
382
+ const res = await this.db.query(sql, [id]);
383
+ if (res.rows.length === 0)
384
+ return null;
385
+ return mapRow(res.rows[0]);
386
+ }
316
387
  /**
317
388
  * Adds an external subject identifier to a feature flag.
318
389
  */
@@ -348,12 +419,13 @@ export class FeatureFlagModel {
348
419
  /**
349
420
  * Gets all feature flags directly associated with an external subject identifier.
350
421
  */
351
- async getFeatureFlagsForSubject(subject) {
422
+ async getFeatureFlagsForSubject(subject, { includeArchived = false } = {}) {
423
+ const filter = includeArchived ? "" : " AND f.archived IS NULL";
352
424
  const sql = `
353
425
  SELECT ${COLUMNS.map((c) => `f.${c}`).join(", ")}
354
426
  FROM ${TABLE} f
355
427
  INNER JOIN ${SUBJECT_TABLE} s ON f.id = s.feature_flag_id
356
- WHERE s.subject = $1
428
+ WHERE s.subject = $1${filter}
357
429
  ORDER BY f.created DESC
358
430
  `;
359
431
  const res = await this.db.query(sql, [subject]);
@@ -363,15 +435,20 @@ export class FeatureFlagModel {
363
435
  if (subjects.length === 0) {
364
436
  return new Set();
365
437
  }
438
+ // Archived flags and archived groups do not contribute subject-based
439
+ // targeting: a flag matched directly must itself be active, and a flag
440
+ // matched via group membership must belong to an active group.
366
441
  const sql = `
367
442
  SELECT s.feature_flag_id AS feature_flag_id
368
443
  FROM ${SUBJECT_TABLE} s
369
- WHERE s.subject = ANY($1)
444
+ INNER JOIN ${TABLE} f ON f.id = s.feature_flag_id
445
+ WHERE s.subject = ANY($1) AND f.archived IS NULL
370
446
  UNION
371
447
  SELECT gm.feature_flag_id AS feature_flag_id
372
448
  FROM ${GROUP_SUBJECT_TABLE} gs
373
449
  INNER JOIN ${GROUP_MEMBER_TABLE} gm ON gs.feature_flag_group_id = gm.group_id
374
- WHERE gs.subject = ANY($1)
450
+ INNER JOIN ${GROUP_TABLE} g ON g.id = gs.feature_flag_group_id
451
+ WHERE gs.subject = ANY($1) AND g.archived IS NULL
375
452
  `;
376
453
  const res = await this.db.query(sql, [subjects]);
377
454
  return new Set(res.rows.map((row) => Number(row.feature_flag_id)));
@@ -652,7 +729,14 @@ export class FeatureFlagModel {
652
729
  }
653
730
  const GROUP_TABLE = "flapjack.feature_flag_group";
654
731
  const GROUP_MEMBER_TABLE = "flapjack.feature_flag_group_member";
655
- const GROUP_COLUMNS = ["id", "name", "note", "created", "modified"];
732
+ const GROUP_COLUMNS = [
733
+ "id",
734
+ "name",
735
+ "note",
736
+ "created",
737
+ "modified",
738
+ "archived",
739
+ ];
656
740
  function mapGroupRow(row) {
657
741
  return {
658
742
  id: row.id,
@@ -660,6 +744,7 @@ function mapGroupRow(row) {
660
744
  note: row.note ?? undefined,
661
745
  created: new Date(row.created),
662
746
  modified: new Date(row.modified),
747
+ archived: row.archived ? new Date(row.archived) : undefined,
663
748
  };
664
749
  }
665
750
  /**
@@ -736,8 +821,9 @@ export class FeatureFlagGroupModel {
736
821
  * }
737
822
  * ```
738
823
  */
739
- async getById(id) {
740
- const sql = `SELECT ${GROUP_COLUMNS.join(", ")} FROM ${GROUP_TABLE} WHERE id = $1`;
824
+ async getById(id, { includeArchived = false } = {}) {
825
+ const filter = includeArchived ? "" : " AND archived IS NULL";
826
+ const sql = `SELECT ${GROUP_COLUMNS.join(", ")} FROM ${GROUP_TABLE} WHERE id = $1${filter}`;
741
827
  const res = await this.db.query(sql, [id]);
742
828
  return res.rows.length > 0 ? mapGroupRow(res.rows[0]) : null;
743
829
  }
@@ -752,8 +838,9 @@ export class FeatureFlagGroupModel {
752
838
  * const group = await model.getByName("billing_redesign");
753
839
  * ```
754
840
  */
755
- async getByName(name) {
756
- const sql = `SELECT ${GROUP_COLUMNS.join(", ")} FROM ${GROUP_TABLE} WHERE name = $1`;
841
+ async getByName(name, { includeArchived = false } = {}) {
842
+ const filter = includeArchived ? "" : " AND archived IS NULL";
843
+ const sql = `SELECT ${GROUP_COLUMNS.join(", ")} FROM ${GROUP_TABLE} WHERE name = $1${filter}`;
757
844
  const res = await this.db.query(sql, [name]);
758
845
  return res.rows.length > 0 ? mapGroupRow(res.rows[0]) : null;
759
846
  }
@@ -770,8 +857,9 @@ export class FeatureFlagGroupModel {
770
857
  * }
771
858
  * ```
772
859
  */
773
- async list() {
774
- const sql = `SELECT ${GROUP_COLUMNS.join(", ")} FROM ${GROUP_TABLE} ORDER BY created DESC`;
860
+ async list({ includeArchived = false, } = {}) {
861
+ const filter = includeArchived ? "" : " WHERE archived IS NULL";
862
+ const sql = `SELECT ${GROUP_COLUMNS.join(", ")} FROM ${GROUP_TABLE}${filter} ORDER BY created DESC`;
775
863
  const res = await this.db.query(sql);
776
864
  return res.rows.map(mapGroupRow);
777
865
  }
@@ -813,11 +901,16 @@ export class FeatureFlagGroupModel {
813
901
  return res.rows.length > 0 ? mapGroupRow(res.rows[0]) : null;
814
902
  }
815
903
  /**
816
- * Deletes a feature flag group and all its member relationships.
904
+ * Permanently deletes a feature flag group and all its member and subject
905
+ * relationships (via ON DELETE CASCADE).
817
906
  *
818
907
  * @param id - The group ID to delete
819
908
  * @returns true if deleted, false if not found
820
909
  *
910
+ * @remarks
911
+ * This is a hard delete that destroys all metadata. To hide a group while
912
+ * retaining its history for audit purposes, use {@link archive} instead.
913
+ *
821
914
  * @example
822
915
  * ```typescript
823
916
  * const deleted = await model.delete(1);
@@ -827,6 +920,31 @@ export class FeatureFlagGroupModel {
827
920
  const res = await this.db.query(`DELETE FROM ${GROUP_TABLE} WHERE id = $1`, [id]);
828
921
  return res.rowCount > 0;
829
922
  }
923
+ /**
924
+ * Archives (hides) a feature flag group while keeping the row for
925
+ * historical/audit purposes. Archived groups are excluded from normal reads
926
+ * and no longer contribute subject-based targeting. Member flags are left
927
+ * untouched and remain active; the membership and subject rows are kept for
928
+ * audit but become inert.
929
+ *
930
+ * @param id - The group ID to archive
931
+ * @returns The archived group, or null if it does not exist or is already archived
932
+ *
933
+ * @remarks
934
+ * Archiving is irreversible: there is intentionally no `unarchive` method.
935
+ * The `archived` timestamp is set once and never changed (a second call is a
936
+ * no-op that returns null). The group's name remains reserved permanently.
937
+ *
938
+ * @example
939
+ * ```typescript
940
+ * const archived = await model.archive(1);
941
+ * ```
942
+ */
943
+ async archive(id) {
944
+ const sql = `UPDATE ${GROUP_TABLE} SET archived = now() WHERE id = $1 AND archived IS NULL RETURNING ${GROUP_COLUMNS.join(", ")}`;
945
+ const res = await this.db.query(sql, [id]);
946
+ return res.rows.length > 0 ? mapGroupRow(res.rows[0]) : null;
947
+ }
830
948
  /**
831
949
  * Adds a feature flag to a group.
832
950
  *
@@ -888,12 +1006,13 @@ export class FeatureFlagGroupModel {
888
1006
  * }
889
1007
  * ```
890
1008
  */
891
- async getFeatureFlags(groupId) {
1009
+ async getFeatureFlags(groupId, { includeArchived = false } = {}) {
1010
+ const filter = includeArchived ? "" : " AND f.archived IS NULL";
892
1011
  const sql = `
893
1012
  SELECT ${COLUMNS.map((c) => `f.${c}`).join(", ")}
894
1013
  FROM ${TABLE} f
895
1014
  INNER JOIN ${GROUP_MEMBER_TABLE} gm ON f.id = gm.feature_flag_id
896
- WHERE gm.group_id = $1
1015
+ WHERE gm.group_id = $1${filter}
897
1016
  ORDER BY f.created DESC
898
1017
  `;
899
1018
  const res = await this.db.query(sql, [groupId]);
@@ -910,12 +1029,13 @@ export class FeatureFlagGroupModel {
910
1029
  * const groups = await model.getGroupsForFeatureFlag(5);
911
1030
  * ```
912
1031
  */
913
- async getGroupsForFeatureFlag(featureFlagId) {
1032
+ async getGroupsForFeatureFlag(featureFlagId, { includeArchived = false } = {}) {
1033
+ const filter = includeArchived ? "" : " AND g.archived IS NULL";
914
1034
  const sql = `
915
1035
  SELECT ${GROUP_COLUMNS.map((c) => `g.${c}`).join(", ")}
916
1036
  FROM ${GROUP_TABLE} g
917
1037
  INNER JOIN ${GROUP_MEMBER_TABLE} gm ON g.id = gm.group_id
918
- WHERE gm.feature_flag_id = $1
1038
+ WHERE gm.feature_flag_id = $1${filter}
919
1039
  ORDER BY g.created DESC
920
1040
  `;
921
1041
  const res = await this.db.query(sql, [featureFlagId]);
@@ -960,12 +1080,13 @@ export class FeatureFlagGroupModel {
960
1080
  /**
961
1081
  * Gets all groups that are associated with a specific external subject identifier.
962
1082
  */
963
- async getGroupsForSubject(subject) {
1083
+ async getGroupsForSubject(subject, { includeArchived = false } = {}) {
1084
+ const filter = includeArchived ? "" : " AND g.archived IS NULL";
964
1085
  const sql = `
965
1086
  SELECT ${GROUP_COLUMNS.map((c) => `g.${c}`).join(", ")}
966
1087
  FROM ${GROUP_TABLE} g
967
1088
  INNER JOIN ${GROUP_SUBJECT_TABLE} gs ON g.id = gs.feature_flag_group_id
968
- WHERE gs.subject = $1
1089
+ WHERE gs.subject = $1${filter}
969
1090
  ORDER BY g.created DESC
970
1091
  `;
971
1092
  const res = await this.db.query(sql, [subject]);
@@ -1019,10 +1140,10 @@ export class FeatureFlagGroupModel {
1019
1140
  }
1020
1141
  vals.push(groupId);
1021
1142
  const sql = `
1022
- UPDATE ${TABLE}
1143
+ UPDATE ${TABLE}
1023
1144
  SET ${updates.join(", ")}
1024
- WHERE id IN (
1025
- SELECT feature_flag_id
1145
+ WHERE archived IS NULL AND id IN (
1146
+ SELECT feature_flag_id
1026
1147
  FROM ${GROUP_MEMBER_TABLE}
1027
1148
  WHERE group_id = $${paramIdx}
1028
1149
  )
package/dist/types.d.ts CHANGED
@@ -16,6 +16,8 @@ export type FeatureFlag = {
16
16
  groups?: string[];
17
17
  /** List of specific user IDs that have this feature flag enabled */
18
18
  users?: string[];
19
+ /** List of tags for organizing flags (e.g. a release version like "release:1.5.0") */
20
+ tags?: string[];
19
21
  /** Description of where this flag is used and what it does */
20
22
  note?: string;
21
23
  /** Date when the feature flag was created */
@@ -24,6 +26,11 @@ export type FeatureFlag = {
24
26
  modified: Date;
25
27
  /** Optional expiration date for the feature flag */
26
28
  expires?: Date;
29
+ /**
30
+ * Date when the feature flag was archived (hidden). Set once and immutable;
31
+ * archiving is irreversible and the name remains reserved permanently.
32
+ */
33
+ archived?: Date;
27
34
  };
28
35
  /**
29
36
  * Called when a feature flag which has expired is used.
@@ -56,5 +63,10 @@ export type FeatureFlagGroup = {
56
63
  created: Date;
57
64
  /** Date when the group was last modified */
58
65
  modified: Date;
66
+ /**
67
+ * Date when the group was archived (hidden). Set once and immutable;
68
+ * archiving is irreversible and the name remains reserved permanently.
69
+ */
70
+ archived?: Date;
59
71
  };
60
72
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,6CAA6C;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,OAAO,EAAE,IAAI,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,IAAI,CAAC;IACf,oDAAoD;IACpD,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,8BAA8B,GAAG,CAAC,EAC5C,IAAI,GACL,EAAE;IACD,IAAI,EAAE,WAAW,CAAC;CACnB,KAAK,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;AAEnC;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,CAAC,EAAE,8BAA8B,CAAC;CAC5C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,OAAO,EAAE,IAAI,CAAC;IACd,4CAA4C;IAC5C,QAAQ,EAAE,IAAI,CAAC;CAChB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,6CAA6C;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,sFAAsF;IACtF,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,OAAO,EAAE,IAAI,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,IAAI,CAAC;IACf,oDAAoD;IACpD,OAAO,CAAC,EAAE,IAAI,CAAC;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,IAAI,CAAC;CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,8BAA8B,GAAG,CAAC,EAC5C,IAAI,GACL,EAAE;IACD,IAAI,EAAE,WAAW,CAAC;CACnB,KAAK,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;AAEnC;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,CAAC,EAAE,8BAA8B,CAAC;CAC5C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,OAAO,EAAE,IAAI,CAAC;IACd,4CAA4C;IAC5C,QAAQ,EAAE,IAAI,CAAC;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,IAAI,CAAC;CACjB,CAAC"}
@@ -0,0 +1,38 @@
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.addColumn(
13
+ { schema: "flapjack", name: "feature_flag" },
14
+ {
15
+ tags: {
16
+ type: "text[]",
17
+ default: pgm.func("'{}'::text[]"),
18
+ },
19
+ },
20
+ );
21
+
22
+ // GIN index for efficient lookups by tag (e.g. WHERE $1 = ANY(tags))
23
+ pgm.createIndex({ schema: "flapjack", name: "feature_flag" }, "tags", {
24
+ method: "gin",
25
+ });
26
+ };
27
+
28
+ /**
29
+ * @param pgm {import('node-pg-migrate').MigrationBuilder}
30
+ * @param run {() => void | undefined}
31
+ * @returns {Promise<void> | void}
32
+ */
33
+ export const down = (pgm) => {
34
+ pgm.dropIndex({ schema: "flapjack", name: "feature_flag" }, "tags", {
35
+ method: "gin",
36
+ });
37
+ pgm.dropColumn({ schema: "flapjack", name: "feature_flag" }, "tags");
38
+ };
@@ -0,0 +1,37 @@
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
+ // Add a nullable archived timestamp to the core data models. When set, the
13
+ // record is considered archived (hidden) but kept for historical/audit
14
+ // purposes. Archiving is irreversible and the name remains reserved forever
15
+ // (the existing UNIQUE(name) constraint is intentionally left unchanged).
16
+ pgm.addColumn(
17
+ { schema: "flapjack", name: "feature_flag" },
18
+ { archived: { type: "timestamptz" } },
19
+ );
20
+ pgm.addColumn(
21
+ { schema: "flapjack", name: "feature_flag_group" },
22
+ { archived: { type: "timestamptz" } },
23
+ );
24
+ };
25
+
26
+ /**
27
+ * @param pgm {import('node-pg-migrate').MigrationBuilder}
28
+ * @param run {() => void | undefined}
29
+ * @returns {Promise<void> | void}
30
+ */
31
+ export const down = (pgm) => {
32
+ pgm.dropColumn({ schema: "flapjack", name: "feature_flag" }, "archived");
33
+ pgm.dropColumn(
34
+ { schema: "flapjack", name: "feature_flag_group" },
35
+ "archived",
36
+ );
37
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandtg/flapjack",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "A simple feature flags library with PostgreSQL integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",