@brandtg/flapjack 1.3.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 +148 -13
- package/dist/cache.d.ts +7 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +19 -3
- package/dist/cli.js +867 -9
- package/dist/model.d.ts +167 -17
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +310 -28
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/migrations/1779210257698_add-feature-flag-subjects.js +93 -0
- package/migrations/1781107651000_add-feature-flag-tags.js +38 -0
- package/migrations/1781200000000_add-archived.js +37 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import yargs from "yargs";
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
4
|
import { Pool } from "pg";
|
|
5
|
-
import { FeatureFlagModel } from "./model.js";
|
|
5
|
+
import { FeatureFlagModel, FeatureFlagGroupModel } from "./model.js";
|
|
6
6
|
import dotenv from "dotenv";
|
|
7
7
|
dotenv.config({ quiet: true });
|
|
8
8
|
function createDatabase() {
|
|
@@ -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
|
|
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
|
|
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", () => {
|
|
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
|
|
@@ -334,6 +421,777 @@ cli.command("is-active <name>", "Check if a feature flag is active for a user",
|
|
|
334
421
|
await db.end();
|
|
335
422
|
}
|
|
336
423
|
});
|
|
424
|
+
// Check multiple flags for user command
|
|
425
|
+
cli.command("are-active", "Check if multiple feature flags are active for a user", (yargs) => {
|
|
426
|
+
return yargs.options({
|
|
427
|
+
names: {
|
|
428
|
+
type: "array",
|
|
429
|
+
describe: "Optional list of feature flag names (defaults to all flags)",
|
|
430
|
+
},
|
|
431
|
+
user: {
|
|
432
|
+
type: "string",
|
|
433
|
+
describe: "User ID",
|
|
434
|
+
},
|
|
435
|
+
roles: {
|
|
436
|
+
type: "array",
|
|
437
|
+
describe: "User roles",
|
|
438
|
+
},
|
|
439
|
+
groups: {
|
|
440
|
+
type: "array",
|
|
441
|
+
describe: "User groups",
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
}, async (argv) => {
|
|
445
|
+
const db = createDatabase();
|
|
446
|
+
const model = new FeatureFlagModel(db);
|
|
447
|
+
try {
|
|
448
|
+
const results = await model.areActiveForUser({
|
|
449
|
+
names: argv.names,
|
|
450
|
+
user: argv.user,
|
|
451
|
+
roles: argv.roles,
|
|
452
|
+
groups: argv.groups,
|
|
453
|
+
});
|
|
454
|
+
console.log(JSON.stringify({
|
|
455
|
+
names: argv.names,
|
|
456
|
+
user: argv.user,
|
|
457
|
+
roles: argv.roles,
|
|
458
|
+
groups: argv.groups,
|
|
459
|
+
results,
|
|
460
|
+
}, null, 2));
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
console.error("Error checking multiple feature flags:", error);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
await db.end();
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
// Check if active for context command
|
|
471
|
+
cli.command("is-active-context <name>", "Check if a feature flag is active for a context including subjects", (yargs) => {
|
|
472
|
+
return yargs
|
|
473
|
+
.positional("name", {
|
|
474
|
+
type: "string",
|
|
475
|
+
describe: "Feature flag name",
|
|
476
|
+
})
|
|
477
|
+
.options({
|
|
478
|
+
user: {
|
|
479
|
+
type: "string",
|
|
480
|
+
describe: "User ID",
|
|
481
|
+
},
|
|
482
|
+
roles: {
|
|
483
|
+
type: "array",
|
|
484
|
+
describe: "User roles",
|
|
485
|
+
},
|
|
486
|
+
groups: {
|
|
487
|
+
type: "array",
|
|
488
|
+
describe: "User groups",
|
|
489
|
+
},
|
|
490
|
+
subjects: {
|
|
491
|
+
type: "array",
|
|
492
|
+
describe: "External subject IDs (for example tenant:acme)",
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
}, async (argv) => {
|
|
496
|
+
const db = createDatabase();
|
|
497
|
+
const model = new FeatureFlagModel(db);
|
|
498
|
+
try {
|
|
499
|
+
const isActive = await model.isActiveForContext({
|
|
500
|
+
name: argv.name,
|
|
501
|
+
user: argv.user,
|
|
502
|
+
roles: argv.roles,
|
|
503
|
+
groups: argv.groups,
|
|
504
|
+
subjects: argv.subjects,
|
|
505
|
+
});
|
|
506
|
+
console.log(JSON.stringify({
|
|
507
|
+
name: argv.name,
|
|
508
|
+
isActive,
|
|
509
|
+
user: argv.user,
|
|
510
|
+
roles: argv.roles,
|
|
511
|
+
groups: argv.groups,
|
|
512
|
+
subjects: argv.subjects,
|
|
513
|
+
}, null, 2));
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
console.error("Error checking feature flag context:", error);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
await db.end();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
// Add a subject to a feature flag
|
|
524
|
+
cli.command("add-subject <id> <subject>", "Add an external subject ID to a feature flag", (yargs) => {
|
|
525
|
+
return yargs
|
|
526
|
+
.positional("id", {
|
|
527
|
+
type: "number",
|
|
528
|
+
describe: "Feature flag ID",
|
|
529
|
+
})
|
|
530
|
+
.positional("subject", {
|
|
531
|
+
type: "string",
|
|
532
|
+
describe: "External subject ID (for example tenant:acme)",
|
|
533
|
+
});
|
|
534
|
+
}, async (argv) => {
|
|
535
|
+
const db = createDatabase();
|
|
536
|
+
const model = new FeatureFlagModel(db);
|
|
537
|
+
try {
|
|
538
|
+
const added = await model.addSubject(argv.id, argv.subject);
|
|
539
|
+
console.log(JSON.stringify({
|
|
540
|
+
id: argv.id,
|
|
541
|
+
subject: argv.subject,
|
|
542
|
+
added,
|
|
543
|
+
}, null, 2));
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
console.error("Error adding subject:", error);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
await db.end();
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
// Remove a subject from a feature flag
|
|
554
|
+
cli.command("remove-subject <id> <subject>", "Remove an external subject ID from a feature flag", (yargs) => {
|
|
555
|
+
return yargs
|
|
556
|
+
.positional("id", {
|
|
557
|
+
type: "number",
|
|
558
|
+
describe: "Feature flag ID",
|
|
559
|
+
})
|
|
560
|
+
.positional("subject", {
|
|
561
|
+
type: "string",
|
|
562
|
+
describe: "External subject ID",
|
|
563
|
+
});
|
|
564
|
+
}, async (argv) => {
|
|
565
|
+
const db = createDatabase();
|
|
566
|
+
const model = new FeatureFlagModel(db);
|
|
567
|
+
try {
|
|
568
|
+
const removed = await model.removeSubject(argv.id, argv.subject);
|
|
569
|
+
console.log(JSON.stringify({
|
|
570
|
+
id: argv.id,
|
|
571
|
+
subject: argv.subject,
|
|
572
|
+
removed,
|
|
573
|
+
}, null, 2));
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
console.error("Error removing subject:", error);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
finally {
|
|
580
|
+
await db.end();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
// List subjects for a feature flag
|
|
584
|
+
cli.command("list-subjects <id>", "List external subject IDs for a feature flag", (yargs) => {
|
|
585
|
+
return yargs.positional("id", {
|
|
586
|
+
type: "number",
|
|
587
|
+
describe: "Feature flag ID",
|
|
588
|
+
});
|
|
589
|
+
}, async (argv) => {
|
|
590
|
+
const db = createDatabase();
|
|
591
|
+
const model = new FeatureFlagModel(db);
|
|
592
|
+
try {
|
|
593
|
+
const subjects = await model.getSubjects(argv.id);
|
|
594
|
+
console.log(JSON.stringify({
|
|
595
|
+
id: argv.id,
|
|
596
|
+
subjects,
|
|
597
|
+
}, null, 2));
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
console.error("Error listing subjects:", error);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
await db.end();
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
// Check multiple flags for context command
|
|
608
|
+
cli.command("are-active-context", "Check if multiple feature flags are active for a context including subjects", (yargs) => {
|
|
609
|
+
return yargs.options({
|
|
610
|
+
names: {
|
|
611
|
+
type: "array",
|
|
612
|
+
describe: "Optional list of feature flag names (defaults to all flags)",
|
|
613
|
+
},
|
|
614
|
+
user: {
|
|
615
|
+
type: "string",
|
|
616
|
+
describe: "User ID",
|
|
617
|
+
},
|
|
618
|
+
roles: {
|
|
619
|
+
type: "array",
|
|
620
|
+
describe: "User roles",
|
|
621
|
+
},
|
|
622
|
+
groups: {
|
|
623
|
+
type: "array",
|
|
624
|
+
describe: "User groups",
|
|
625
|
+
},
|
|
626
|
+
subjects: {
|
|
627
|
+
type: "array",
|
|
628
|
+
describe: "External subject IDs (for example tenant:acme)",
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
}, async (argv) => {
|
|
632
|
+
const db = createDatabase();
|
|
633
|
+
const model = new FeatureFlagModel(db);
|
|
634
|
+
try {
|
|
635
|
+
const results = await model.areActiveForContext({
|
|
636
|
+
names: argv.names,
|
|
637
|
+
user: argv.user,
|
|
638
|
+
roles: argv.roles,
|
|
639
|
+
groups: argv.groups,
|
|
640
|
+
subjects: argv.subjects,
|
|
641
|
+
});
|
|
642
|
+
console.log(JSON.stringify({
|
|
643
|
+
names: argv.names,
|
|
644
|
+
user: argv.user,
|
|
645
|
+
roles: argv.roles,
|
|
646
|
+
groups: argv.groups,
|
|
647
|
+
subjects: argv.subjects,
|
|
648
|
+
results,
|
|
649
|
+
}, null, 2));
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
console.error("Error checking multiple feature flag contexts:", error);
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
finally {
|
|
656
|
+
await db.end();
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
// List flags that directly match a subject
|
|
660
|
+
cli.command("list-by-subject <subject>", "List feature flags directly assigned to an external subject ID", (yargs) => {
|
|
661
|
+
return yargs.positional("subject", {
|
|
662
|
+
type: "string",
|
|
663
|
+
describe: "External subject ID",
|
|
664
|
+
});
|
|
665
|
+
}, async (argv) => {
|
|
666
|
+
const db = createDatabase();
|
|
667
|
+
const model = new FeatureFlagModel(db);
|
|
668
|
+
try {
|
|
669
|
+
const flags = await model.getFeatureFlagsForSubject(argv.subject);
|
|
670
|
+
console.log(JSON.stringify({
|
|
671
|
+
subject: argv.subject,
|
|
672
|
+
flags,
|
|
673
|
+
}, null, 2));
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
console.error("Error listing flags by subject:", error);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
finally {
|
|
680
|
+
await db.end();
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
// Create feature flag group command
|
|
684
|
+
cli.command("group-create", "Create a feature flag group", (yargs) => {
|
|
685
|
+
return yargs
|
|
686
|
+
.options({
|
|
687
|
+
name: {
|
|
688
|
+
type: "string",
|
|
689
|
+
describe: "Feature flag group name",
|
|
690
|
+
},
|
|
691
|
+
note: {
|
|
692
|
+
type: "string",
|
|
693
|
+
describe: "Feature flag group description",
|
|
694
|
+
},
|
|
695
|
+
})
|
|
696
|
+
.demandOption("name", "Feature flag group name is required");
|
|
697
|
+
}, async (argv) => {
|
|
698
|
+
const db = createDatabase();
|
|
699
|
+
const model = new FeatureFlagGroupModel(db);
|
|
700
|
+
try {
|
|
701
|
+
const group = await model.create({
|
|
702
|
+
name: argv.name,
|
|
703
|
+
note: argv.note,
|
|
704
|
+
});
|
|
705
|
+
console.log(JSON.stringify(group, null, 2));
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
console.error("Error creating feature flag group:", error);
|
|
709
|
+
process.exit(1);
|
|
710
|
+
}
|
|
711
|
+
finally {
|
|
712
|
+
await db.end();
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
// List feature flag groups command
|
|
716
|
+
cli.command("group-list", "List all feature flag groups", (yargs) => {
|
|
717
|
+
return yargs.options(includeArchivedOption);
|
|
718
|
+
}, async (argv) => {
|
|
719
|
+
const db = createDatabase();
|
|
720
|
+
const model = new FeatureFlagGroupModel(db);
|
|
721
|
+
try {
|
|
722
|
+
const groups = await model.list({
|
|
723
|
+
includeArchived: argv.includeArchived,
|
|
724
|
+
});
|
|
725
|
+
console.log(JSON.stringify(groups, null, 2));
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
console.error("Error listing feature flag groups:", error);
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
await db.end();
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
// Get feature flag group by ID command
|
|
736
|
+
cli.command("group-get <id>", "Get a feature flag group by ID", (yargs) => {
|
|
737
|
+
return yargs
|
|
738
|
+
.positional("id", {
|
|
739
|
+
type: "number",
|
|
740
|
+
describe: "Feature flag group ID",
|
|
741
|
+
})
|
|
742
|
+
.options(includeArchivedOption);
|
|
743
|
+
}, async (argv) => {
|
|
744
|
+
const db = createDatabase();
|
|
745
|
+
const model = new FeatureFlagGroupModel(db);
|
|
746
|
+
try {
|
|
747
|
+
const group = await model.getById(argv.id, {
|
|
748
|
+
includeArchived: argv.includeArchived,
|
|
749
|
+
});
|
|
750
|
+
if (!group) {
|
|
751
|
+
console.log(`Feature flag group with ID ${argv.id} not found`);
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
console.log(JSON.stringify(group, null, 2));
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
console.error("Error getting feature flag group:", error);
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
finally {
|
|
761
|
+
await db.end();
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
// Get feature flag group by name command
|
|
765
|
+
cli.command("group-get-by-name <name>", "Get a feature flag group by name", (yargs) => {
|
|
766
|
+
return yargs
|
|
767
|
+
.positional("name", {
|
|
768
|
+
type: "string",
|
|
769
|
+
describe: "Feature flag group name",
|
|
770
|
+
})
|
|
771
|
+
.options(includeArchivedOption);
|
|
772
|
+
}, async (argv) => {
|
|
773
|
+
const db = createDatabase();
|
|
774
|
+
const model = new FeatureFlagGroupModel(db);
|
|
775
|
+
try {
|
|
776
|
+
const group = await model.getByName(argv.name, {
|
|
777
|
+
includeArchived: argv.includeArchived,
|
|
778
|
+
});
|
|
779
|
+
if (!group) {
|
|
780
|
+
console.log(`Feature flag group with name "${argv.name}" not found`);
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
console.log(JSON.stringify(group, null, 2));
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
console.error("Error getting feature flag group by name:", error);
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
finally {
|
|
790
|
+
await db.end();
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
// Update feature flag group command
|
|
794
|
+
cli.command("group-update <id>", "Update a feature flag group", (yargs) => {
|
|
795
|
+
return yargs
|
|
796
|
+
.positional("id", {
|
|
797
|
+
type: "number",
|
|
798
|
+
describe: "Feature flag group ID",
|
|
799
|
+
})
|
|
800
|
+
.options({
|
|
801
|
+
name: {
|
|
802
|
+
type: "string",
|
|
803
|
+
describe: "Feature flag group name",
|
|
804
|
+
},
|
|
805
|
+
note: {
|
|
806
|
+
type: "string",
|
|
807
|
+
describe: "Feature flag group description",
|
|
808
|
+
},
|
|
809
|
+
"clear-note": {
|
|
810
|
+
type: "boolean",
|
|
811
|
+
describe: "Clear the group note",
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
}, async (argv) => {
|
|
815
|
+
const db = createDatabase();
|
|
816
|
+
const model = new FeatureFlagGroupModel(db);
|
|
817
|
+
try {
|
|
818
|
+
const changes = {};
|
|
819
|
+
if (argv.name !== undefined)
|
|
820
|
+
changes.name = argv.name;
|
|
821
|
+
if (argv.clearNote)
|
|
822
|
+
changes.note = null;
|
|
823
|
+
else if (argv.note !== undefined)
|
|
824
|
+
changes.note = argv.note;
|
|
825
|
+
const group = await model.update(argv.id, changes);
|
|
826
|
+
if (!group) {
|
|
827
|
+
console.log(`Feature flag group with ID ${argv.id} not found`);
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
console.log(JSON.stringify(group, null, 2));
|
|
831
|
+
}
|
|
832
|
+
catch (error) {
|
|
833
|
+
console.error("Error updating feature flag group:", error);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
finally {
|
|
837
|
+
await db.end();
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
// Delete feature flag group command
|
|
841
|
+
cli.command("group-delete <id>", "Delete a feature flag group", (yargs) => {
|
|
842
|
+
return yargs.positional("id", {
|
|
843
|
+
type: "number",
|
|
844
|
+
describe: "Feature flag group ID",
|
|
845
|
+
});
|
|
846
|
+
}, async (argv) => {
|
|
847
|
+
const db = createDatabase();
|
|
848
|
+
const model = new FeatureFlagGroupModel(db);
|
|
849
|
+
try {
|
|
850
|
+
const deleted = await model.delete(argv.id);
|
|
851
|
+
if (!deleted) {
|
|
852
|
+
console.log(`Feature flag group with ID ${argv.id} not found`);
|
|
853
|
+
process.exit(1);
|
|
854
|
+
}
|
|
855
|
+
console.log(`Feature flag group with ID ${argv.id} deleted successfully`);
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
console.error("Error deleting feature flag group:", error);
|
|
859
|
+
process.exit(1);
|
|
860
|
+
}
|
|
861
|
+
finally {
|
|
862
|
+
await db.end();
|
|
863
|
+
}
|
|
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
|
+
});
|
|
890
|
+
// Add flag to feature flag group command
|
|
891
|
+
cli.command("group-add-flag <groupId> <flagId>", "Add a feature flag to a group", (yargs) => {
|
|
892
|
+
return yargs
|
|
893
|
+
.positional("groupId", {
|
|
894
|
+
type: "number",
|
|
895
|
+
describe: "Feature flag group ID",
|
|
896
|
+
})
|
|
897
|
+
.positional("flagId", {
|
|
898
|
+
type: "number",
|
|
899
|
+
describe: "Feature flag ID",
|
|
900
|
+
});
|
|
901
|
+
}, async (argv) => {
|
|
902
|
+
const db = createDatabase();
|
|
903
|
+
const model = new FeatureFlagGroupModel(db);
|
|
904
|
+
try {
|
|
905
|
+
const added = await model.addFeatureFlag(argv.groupId, argv.flagId);
|
|
906
|
+
console.log(JSON.stringify({
|
|
907
|
+
groupId: argv.groupId,
|
|
908
|
+
flagId: argv.flagId,
|
|
909
|
+
added,
|
|
910
|
+
}, null, 2));
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
console.error("Error adding feature flag to group:", error);
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
finally {
|
|
917
|
+
await db.end();
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
// Remove flag from feature flag group command
|
|
921
|
+
cli.command("group-remove-flag <groupId> <flagId>", "Remove a feature flag from a group", (yargs) => {
|
|
922
|
+
return yargs
|
|
923
|
+
.positional("groupId", {
|
|
924
|
+
type: "number",
|
|
925
|
+
describe: "Feature flag group ID",
|
|
926
|
+
})
|
|
927
|
+
.positional("flagId", {
|
|
928
|
+
type: "number",
|
|
929
|
+
describe: "Feature flag ID",
|
|
930
|
+
});
|
|
931
|
+
}, async (argv) => {
|
|
932
|
+
const db = createDatabase();
|
|
933
|
+
const model = new FeatureFlagGroupModel(db);
|
|
934
|
+
try {
|
|
935
|
+
const removed = await model.removeFeatureFlag(argv.groupId, argv.flagId);
|
|
936
|
+
console.log(JSON.stringify({
|
|
937
|
+
groupId: argv.groupId,
|
|
938
|
+
flagId: argv.flagId,
|
|
939
|
+
removed,
|
|
940
|
+
}, null, 2));
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
console.error("Error removing feature flag from group:", error);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
finally {
|
|
947
|
+
await db.end();
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
// List flags in feature flag group command
|
|
951
|
+
cli.command("group-list-flags <groupId>", "List all feature flags in a group", (yargs) => {
|
|
952
|
+
return yargs.positional("groupId", {
|
|
953
|
+
type: "number",
|
|
954
|
+
describe: "Feature flag group ID",
|
|
955
|
+
});
|
|
956
|
+
}, async (argv) => {
|
|
957
|
+
const db = createDatabase();
|
|
958
|
+
const model = new FeatureFlagGroupModel(db);
|
|
959
|
+
try {
|
|
960
|
+
const flags = await model.getFeatureFlags(argv.groupId);
|
|
961
|
+
console.log(JSON.stringify({
|
|
962
|
+
groupId: argv.groupId,
|
|
963
|
+
flags,
|
|
964
|
+
}, null, 2));
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
console.error("Error listing flags for group:", error);
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
finally {
|
|
971
|
+
await db.end();
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
// List groups for feature flag command
|
|
975
|
+
cli.command("group-list-for-flag <flagId>", "List all groups that contain a feature flag", (yargs) => {
|
|
976
|
+
return yargs.positional("flagId", {
|
|
977
|
+
type: "number",
|
|
978
|
+
describe: "Feature flag ID",
|
|
979
|
+
});
|
|
980
|
+
}, async (argv) => {
|
|
981
|
+
const db = createDatabase();
|
|
982
|
+
const model = new FeatureFlagGroupModel(db);
|
|
983
|
+
try {
|
|
984
|
+
const groups = await model.getGroupsForFeatureFlag(argv.flagId);
|
|
985
|
+
console.log(JSON.stringify({
|
|
986
|
+
flagId: argv.flagId,
|
|
987
|
+
groups,
|
|
988
|
+
}, null, 2));
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
console.error("Error listing groups for feature flag:", error);
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
finally {
|
|
995
|
+
await db.end();
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
// Bulk update flags in a group command
|
|
999
|
+
cli.command("group-update-all <groupId>", "Update all feature flags in a group", (yargs) => {
|
|
1000
|
+
return yargs
|
|
1001
|
+
.positional("groupId", {
|
|
1002
|
+
type: "number",
|
|
1003
|
+
describe: "Feature flag group ID",
|
|
1004
|
+
})
|
|
1005
|
+
.options({
|
|
1006
|
+
everyone: {
|
|
1007
|
+
type: "boolean",
|
|
1008
|
+
describe: "Enable flag for everyone (overrides all other settings)",
|
|
1009
|
+
},
|
|
1010
|
+
percent: {
|
|
1011
|
+
type: "number",
|
|
1012
|
+
describe: "Percentage rollout (0-99.9)",
|
|
1013
|
+
},
|
|
1014
|
+
roles: {
|
|
1015
|
+
type: "array",
|
|
1016
|
+
describe: "List of roles that have this flag enabled",
|
|
1017
|
+
},
|
|
1018
|
+
groups: {
|
|
1019
|
+
type: "array",
|
|
1020
|
+
describe: "List of user groups that have this flag enabled",
|
|
1021
|
+
},
|
|
1022
|
+
users: {
|
|
1023
|
+
type: "array",
|
|
1024
|
+
describe: "List of specific user IDs that have this flag enabled",
|
|
1025
|
+
},
|
|
1026
|
+
"clear-everyone": {
|
|
1027
|
+
type: "boolean",
|
|
1028
|
+
describe: "Unset the everyone override",
|
|
1029
|
+
},
|
|
1030
|
+
"clear-percent": {
|
|
1031
|
+
type: "boolean",
|
|
1032
|
+
describe: "Unset percentage rollout",
|
|
1033
|
+
},
|
|
1034
|
+
"clear-roles": {
|
|
1035
|
+
type: "boolean",
|
|
1036
|
+
describe: "Clear roles list",
|
|
1037
|
+
},
|
|
1038
|
+
"clear-groups": {
|
|
1039
|
+
type: "boolean",
|
|
1040
|
+
describe: "Clear groups list",
|
|
1041
|
+
},
|
|
1042
|
+
"clear-users": {
|
|
1043
|
+
type: "boolean",
|
|
1044
|
+
describe: "Clear users list",
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
}, async (argv) => {
|
|
1048
|
+
const db = createDatabase();
|
|
1049
|
+
const model = new FeatureFlagGroupModel(db);
|
|
1050
|
+
try {
|
|
1051
|
+
const changes = {};
|
|
1052
|
+
if (argv.clearEveryone)
|
|
1053
|
+
changes.everyone = null;
|
|
1054
|
+
else if (argv.everyone !== undefined)
|
|
1055
|
+
changes.everyone = argv.everyone;
|
|
1056
|
+
if (argv.clearPercent)
|
|
1057
|
+
changes.percent = null;
|
|
1058
|
+
else if (argv.percent !== undefined)
|
|
1059
|
+
changes.percent = argv.percent;
|
|
1060
|
+
if (argv.clearRoles)
|
|
1061
|
+
changes.roles = null;
|
|
1062
|
+
else if (argv.roles !== undefined)
|
|
1063
|
+
changes.roles = argv.roles;
|
|
1064
|
+
if (argv.clearGroups)
|
|
1065
|
+
changes.groups = null;
|
|
1066
|
+
else if (argv.groups !== undefined)
|
|
1067
|
+
changes.groups = argv.groups;
|
|
1068
|
+
if (argv.clearUsers)
|
|
1069
|
+
changes.users = null;
|
|
1070
|
+
else if (argv.users !== undefined)
|
|
1071
|
+
changes.users = argv.users;
|
|
1072
|
+
const updatedCount = await model.updateAll(argv.groupId, changes);
|
|
1073
|
+
console.log(JSON.stringify({
|
|
1074
|
+
groupId: argv.groupId,
|
|
1075
|
+
updatedCount,
|
|
1076
|
+
changes,
|
|
1077
|
+
}, null, 2));
|
|
1078
|
+
}
|
|
1079
|
+
catch (error) {
|
|
1080
|
+
console.error("Error updating all flags in group:", error);
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
finally {
|
|
1084
|
+
await db.end();
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
// Add subject to feature flag group command
|
|
1088
|
+
cli.command("group-add-subject <groupId> <subject>", "Add an external subject ID to a feature flag group", (yargs) => {
|
|
1089
|
+
return yargs
|
|
1090
|
+
.positional("groupId", {
|
|
1091
|
+
type: "number",
|
|
1092
|
+
describe: "Feature flag group ID",
|
|
1093
|
+
})
|
|
1094
|
+
.positional("subject", {
|
|
1095
|
+
type: "string",
|
|
1096
|
+
describe: "External subject ID",
|
|
1097
|
+
});
|
|
1098
|
+
}, async (argv) => {
|
|
1099
|
+
const db = createDatabase();
|
|
1100
|
+
const model = new FeatureFlagGroupModel(db);
|
|
1101
|
+
try {
|
|
1102
|
+
const added = await model.addSubject(argv.groupId, argv.subject);
|
|
1103
|
+
console.log(JSON.stringify({
|
|
1104
|
+
groupId: argv.groupId,
|
|
1105
|
+
subject: argv.subject,
|
|
1106
|
+
added,
|
|
1107
|
+
}, null, 2));
|
|
1108
|
+
}
|
|
1109
|
+
catch (error) {
|
|
1110
|
+
console.error("Error adding subject to group:", error);
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
}
|
|
1113
|
+
finally {
|
|
1114
|
+
await db.end();
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
// Remove subject from feature flag group command
|
|
1118
|
+
cli.command("group-remove-subject <groupId> <subject>", "Remove an external subject ID from a feature flag group", (yargs) => {
|
|
1119
|
+
return yargs
|
|
1120
|
+
.positional("groupId", {
|
|
1121
|
+
type: "number",
|
|
1122
|
+
describe: "Feature flag group ID",
|
|
1123
|
+
})
|
|
1124
|
+
.positional("subject", {
|
|
1125
|
+
type: "string",
|
|
1126
|
+
describe: "External subject ID",
|
|
1127
|
+
});
|
|
1128
|
+
}, async (argv) => {
|
|
1129
|
+
const db = createDatabase();
|
|
1130
|
+
const model = new FeatureFlagGroupModel(db);
|
|
1131
|
+
try {
|
|
1132
|
+
const removed = await model.removeSubject(argv.groupId, argv.subject);
|
|
1133
|
+
console.log(JSON.stringify({
|
|
1134
|
+
groupId: argv.groupId,
|
|
1135
|
+
subject: argv.subject,
|
|
1136
|
+
removed,
|
|
1137
|
+
}, null, 2));
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
console.error("Error removing subject from group:", error);
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
finally {
|
|
1144
|
+
await db.end();
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
// List subjects for feature flag group command
|
|
1148
|
+
cli.command("group-list-subjects <groupId>", "List external subject IDs for a feature flag group", (yargs) => {
|
|
1149
|
+
return yargs.positional("groupId", {
|
|
1150
|
+
type: "number",
|
|
1151
|
+
describe: "Feature flag group ID",
|
|
1152
|
+
});
|
|
1153
|
+
}, async (argv) => {
|
|
1154
|
+
const db = createDatabase();
|
|
1155
|
+
const model = new FeatureFlagGroupModel(db);
|
|
1156
|
+
try {
|
|
1157
|
+
const subjects = await model.getSubjects(argv.groupId);
|
|
1158
|
+
console.log(JSON.stringify({
|
|
1159
|
+
groupId: argv.groupId,
|
|
1160
|
+
subjects,
|
|
1161
|
+
}, null, 2));
|
|
1162
|
+
}
|
|
1163
|
+
catch (error) {
|
|
1164
|
+
console.error("Error listing subjects for group:", error);
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
}
|
|
1167
|
+
finally {
|
|
1168
|
+
await db.end();
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
// List groups for subject command
|
|
1172
|
+
cli.command("group-list-by-subject <subject>", "List feature flag groups assigned to an external subject ID", (yargs) => {
|
|
1173
|
+
return yargs.positional("subject", {
|
|
1174
|
+
type: "string",
|
|
1175
|
+
describe: "External subject ID",
|
|
1176
|
+
});
|
|
1177
|
+
}, async (argv) => {
|
|
1178
|
+
const db = createDatabase();
|
|
1179
|
+
const model = new FeatureFlagGroupModel(db);
|
|
1180
|
+
try {
|
|
1181
|
+
const groups = await model.getGroupsForSubject(argv.subject);
|
|
1182
|
+
console.log(JSON.stringify({
|
|
1183
|
+
subject: argv.subject,
|
|
1184
|
+
groups,
|
|
1185
|
+
}, null, 2));
|
|
1186
|
+
}
|
|
1187
|
+
catch (error) {
|
|
1188
|
+
console.error("Error listing groups by subject:", error);
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
finally {
|
|
1192
|
+
await db.end();
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
337
1195
|
// Hash user ID command (utility)
|
|
338
1196
|
cli.command("hash-user <userId>", "Hash a user ID using the same algorithm as percentage rollout", (yargs) => {
|
|
339
1197
|
return yargs.positional("userId", {
|