@brandtg/flapjack 0.1.2 → 0.2.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 +7 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +206 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +350 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/migrations/1764560643657_add-feature-flag-groups.js +77 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -480,6 +480,12 @@ flapjack hash-user user_123
|
|
|
480
480
|
|
|
481
481
|
### Setup
|
|
482
482
|
|
|
483
|
+
Install dependencies
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
npm install
|
|
487
|
+
```
|
|
488
|
+
|
|
483
489
|
Create an environment file:
|
|
484
490
|
|
|
485
491
|
```bash
|
|
@@ -501,7 +507,7 @@ npm run dev:migrate
|
|
|
501
507
|
### Running Tests
|
|
502
508
|
|
|
503
509
|
```bash
|
|
504
|
-
npm test
|
|
510
|
+
npm run test
|
|
505
511
|
```
|
|
506
512
|
|
|
507
513
|
### Database Management
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { FeatureFlag } from "./types.js";
|
|
2
|
-
export { FeatureFlagModel } from "./model.js";
|
|
1
|
+
export type { FeatureFlag, FeatureFlagGroup } from "./types.js";
|
|
2
|
+
export { FeatureFlagModel, FeatureFlagGroupModel } from "./model.js";
|
|
3
3
|
export { FeatureFlagCache, InMemoryCache, type Cache } from "./cache.js";
|
|
4
4
|
export { runMigrations, type MigrationOptions } from "./migrate.js";
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FeatureFlag, FeatureFlagEventHandlers } from "./types.js";
|
|
1
|
+
import type { FeatureFlag, FeatureFlagEventHandlers, FeatureFlagGroup } from "./types.js";
|
|
2
2
|
import type { QueryResult } from "pg";
|
|
3
3
|
interface Queryable {
|
|
4
4
|
query: (text: string, params?: any[]) => Promise<QueryResult>;
|
|
@@ -89,6 +89,19 @@ export declare class FeatureFlagModel {
|
|
|
89
89
|
* ```
|
|
90
90
|
*/
|
|
91
91
|
getByName(name: string): Promise<FeatureFlag | null>;
|
|
92
|
+
/**
|
|
93
|
+
* Retrieves multiple feature flags by their IDs.
|
|
94
|
+
*
|
|
95
|
+
* @param ids - Array of feature flag IDs to retrieve
|
|
96
|
+
* @returns Array of feature flags found (may be shorter than input if some IDs don't exist)
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const flags = await model.getMany([1, 2, 3]);
|
|
101
|
+
* console.log(`Found ${flags.length} flags`);
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
getMany(ids: number[]): Promise<FeatureFlag[]>;
|
|
92
105
|
/**
|
|
93
106
|
* Retrieves all feature flags, ordered by ID.
|
|
94
107
|
*
|
|
@@ -211,5 +224,197 @@ export declare class FeatureFlagModel {
|
|
|
211
224
|
groups?: string[];
|
|
212
225
|
}): Promise<boolean>;
|
|
213
226
|
}
|
|
227
|
+
type CreateGroupInput = Omit<FeatureFlagGroup, "id" | "created" | "modified">;
|
|
228
|
+
type UpdateGroupChanges = Partial<Omit<FeatureFlagGroup, "id" | "created" | "modified">>;
|
|
229
|
+
type UpdateAllChanges = Partial<Pick<FeatureFlag, "everyone" | "percent" | "roles" | "groups" | "users">>;
|
|
230
|
+
/**
|
|
231
|
+
* Model for managing feature flag groups stored in PostgreSQL.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```typescript
|
|
235
|
+
* import { Pool } from "pg";
|
|
236
|
+
* import { FeatureFlagGroupModel } from "@brandtg/flapjack";
|
|
237
|
+
*
|
|
238
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
239
|
+
* const groups = new FeatureFlagGroupModel(pool);
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
export declare class FeatureFlagGroupModel {
|
|
243
|
+
private db;
|
|
244
|
+
/**
|
|
245
|
+
* Creates a new FeatureFlagGroupModel instance.
|
|
246
|
+
*
|
|
247
|
+
* @param db - A PostgreSQL Pool or Client instance that implements the Queryable interface
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
252
|
+
* const model = new FeatureFlagGroupModel(pool);
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
constructor(db: Queryable);
|
|
256
|
+
/**
|
|
257
|
+
* Creates a new feature flag group in the database.
|
|
258
|
+
*
|
|
259
|
+
* @param input - Feature flag group configuration
|
|
260
|
+
* @param input.name - Unique name for the group (required)
|
|
261
|
+
* @param input.note - Optional description of the group's purpose
|
|
262
|
+
* @returns The created group with generated id, created, and modified timestamps
|
|
263
|
+
*
|
|
264
|
+
* @throws Will throw an error if a group with the same name already exists
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* const group = await model.create({
|
|
269
|
+
* name: "billing_redesign",
|
|
270
|
+
* note: "All flags related to the new billing flow",
|
|
271
|
+
* });
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
create(input: CreateGroupInput): Promise<FeatureFlagGroup>;
|
|
275
|
+
/**
|
|
276
|
+
* Retrieves a feature flag group by its ID.
|
|
277
|
+
*
|
|
278
|
+
* @param id - The group ID
|
|
279
|
+
* @returns The group if found, null otherwise
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* const group = await model.getById(1);
|
|
284
|
+
* if (group) {
|
|
285
|
+
* console.log(group.name);
|
|
286
|
+
* }
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
getById(id: number): Promise<FeatureFlagGroup | null>;
|
|
290
|
+
/**
|
|
291
|
+
* Retrieves a feature flag group by its name.
|
|
292
|
+
*
|
|
293
|
+
* @param name - The group name
|
|
294
|
+
* @returns The group if found, null otherwise
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* const group = await model.getByName("billing_redesign");
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
getByName(name: string): Promise<FeatureFlagGroup | null>;
|
|
302
|
+
/**
|
|
303
|
+
* Lists all feature flag groups.
|
|
304
|
+
*
|
|
305
|
+
* @returns Array of all groups
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```typescript
|
|
309
|
+
* const groups = await model.list();
|
|
310
|
+
* for (const group of groups) {
|
|
311
|
+
* console.log(group.name);
|
|
312
|
+
* }
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
list(): Promise<FeatureFlagGroup[]>;
|
|
316
|
+
/**
|
|
317
|
+
* Updates a feature flag group.
|
|
318
|
+
*
|
|
319
|
+
* @param id - The group ID to update
|
|
320
|
+
* @param changes - Fields to update
|
|
321
|
+
* @returns The updated group if found, null otherwise
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```typescript
|
|
325
|
+
* const updated = await model.update(1, {
|
|
326
|
+
* note: "Updated description",
|
|
327
|
+
* });
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
update(id: number, changes: UpdateGroupChanges): Promise<FeatureFlagGroup | null>;
|
|
331
|
+
/**
|
|
332
|
+
* Deletes a feature flag group and all its member relationships.
|
|
333
|
+
*
|
|
334
|
+
* @param id - The group ID to delete
|
|
335
|
+
* @returns true if deleted, false if not found
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* const deleted = await model.delete(1);
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
delete(id: number): Promise<boolean>;
|
|
343
|
+
/**
|
|
344
|
+
* Adds a feature flag to a group.
|
|
345
|
+
*
|
|
346
|
+
* @param groupId - The group ID
|
|
347
|
+
* @param featureFlagId - The feature flag ID to add
|
|
348
|
+
* @returns true if added successfully, false if already exists
|
|
349
|
+
*
|
|
350
|
+
* @throws Will throw an error if group or feature flag doesn't exist
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```typescript
|
|
354
|
+
* await model.addFeatureFlag(1, 5);
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
addFeatureFlag(groupId: number, featureFlagId: number): Promise<boolean>;
|
|
358
|
+
/**
|
|
359
|
+
* Removes a feature flag from a group.
|
|
360
|
+
*
|
|
361
|
+
* @param groupId - The group ID
|
|
362
|
+
* @param featureFlagId - The feature flag ID to remove
|
|
363
|
+
* @returns true if removed, false if relationship didn't exist
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```typescript
|
|
367
|
+
* await model.removeFeatureFlag(1, 5);
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
removeFeatureFlag(groupId: number, featureFlagId: number): Promise<boolean>;
|
|
371
|
+
/**
|
|
372
|
+
* Gets all feature flags in a group.
|
|
373
|
+
*
|
|
374
|
+
* @param groupId - The group ID
|
|
375
|
+
* @returns Array of feature flags in the group
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* ```typescript
|
|
379
|
+
* const flags = await model.getFeatureFlags(1);
|
|
380
|
+
* for (const flag of flags) {
|
|
381
|
+
* console.log(flag.name);
|
|
382
|
+
* }
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
getFeatureFlags(groupId: number): Promise<FeatureFlag[]>;
|
|
386
|
+
/**
|
|
387
|
+
* Gets all groups that contain a specific feature flag.
|
|
388
|
+
*
|
|
389
|
+
* @param featureFlagId - The feature flag ID
|
|
390
|
+
* @returns Array of groups containing the feature flag
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```typescript
|
|
394
|
+
* const groups = await model.getGroupsForFeatureFlag(5);
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
397
|
+
getGroupsForFeatureFlag(featureFlagId: number): Promise<FeatureFlagGroup[]>;
|
|
398
|
+
/**
|
|
399
|
+
* Updates all feature flags in a group with the same changes.
|
|
400
|
+
*
|
|
401
|
+
* @param groupId - The group ID
|
|
402
|
+
* @param changes - Fields to update (only everyone, percent, roles, groups, users allowed)
|
|
403
|
+
* @returns The number of feature flags updated
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```typescript
|
|
407
|
+
* // Enable all flags in a group for everyone
|
|
408
|
+
* const count = await model.updateAll(1, { everyone: true });
|
|
409
|
+
*
|
|
410
|
+
* // Set percentage rollout for all flags in a group
|
|
411
|
+
* const count = await model.updateAll(1, { percent: 25 });
|
|
412
|
+
*
|
|
413
|
+
* // Add roles to all flags in a group
|
|
414
|
+
* const count = await model.updateAll(1, { roles: ["admin", "beta"] });
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
updateAll(groupId: number, changes: UpdateAllChanges): Promise<number>;
|
|
418
|
+
}
|
|
214
419
|
export {};
|
|
215
420
|
//# sourceMappingURL=model.d.ts.map
|
package/dist/model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
|
package/dist/model.js
CHANGED
|
@@ -166,6 +166,25 @@ export class FeatureFlagModel {
|
|
|
166
166
|
return null;
|
|
167
167
|
return mapRow(res.rows[0]);
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Retrieves multiple feature flags by their IDs.
|
|
171
|
+
*
|
|
172
|
+
* @param ids - Array of feature flag IDs to retrieve
|
|
173
|
+
* @returns Array of feature flags found (may be shorter than input if some IDs don't exist)
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```typescript
|
|
177
|
+
* const flags = await model.getMany([1, 2, 3]);
|
|
178
|
+
* console.log(`Found ${flags.length} flags`);
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
async getMany(ids) {
|
|
182
|
+
if (ids.length === 0)
|
|
183
|
+
return [];
|
|
184
|
+
const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE id = ANY($1)`;
|
|
185
|
+
const res = await this.db.query(sql, [ids]);
|
|
186
|
+
return res.rows.map(mapRow);
|
|
187
|
+
}
|
|
169
188
|
/**
|
|
170
189
|
* Retrieves all feature flags, ordered by ID.
|
|
171
190
|
*
|
|
@@ -387,3 +406,334 @@ export class FeatureFlagModel {
|
|
|
387
406
|
return false;
|
|
388
407
|
}
|
|
389
408
|
}
|
|
409
|
+
const GROUP_TABLE = "flapjack_feature_flag_group";
|
|
410
|
+
const GROUP_MEMBER_TABLE = "flapjack_feature_flag_group_member";
|
|
411
|
+
const GROUP_COLUMNS = ["id", "name", "note", "created", "modified"];
|
|
412
|
+
function mapGroupRow(row) {
|
|
413
|
+
return {
|
|
414
|
+
id: row.id,
|
|
415
|
+
name: row.name,
|
|
416
|
+
note: row.note ?? undefined,
|
|
417
|
+
created: new Date(row.created),
|
|
418
|
+
modified: new Date(row.modified),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Model for managing feature flag groups stored in PostgreSQL.
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```typescript
|
|
426
|
+
* import { Pool } from "pg";
|
|
427
|
+
* import { FeatureFlagGroupModel } from "@brandtg/flapjack";
|
|
428
|
+
*
|
|
429
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
430
|
+
* const groups = new FeatureFlagGroupModel(pool);
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
433
|
+
export class FeatureFlagGroupModel {
|
|
434
|
+
db;
|
|
435
|
+
/**
|
|
436
|
+
* Creates a new FeatureFlagGroupModel instance.
|
|
437
|
+
*
|
|
438
|
+
* @param db - A PostgreSQL Pool or Client instance that implements the Queryable interface
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```typescript
|
|
442
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
443
|
+
* const model = new FeatureFlagGroupModel(pool);
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
constructor(db) {
|
|
447
|
+
this.db = db;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Creates a new feature flag group in the database.
|
|
451
|
+
*
|
|
452
|
+
* @param input - Feature flag group configuration
|
|
453
|
+
* @param input.name - Unique name for the group (required)
|
|
454
|
+
* @param input.note - Optional description of the group's purpose
|
|
455
|
+
* @returns The created group with generated id, created, and modified timestamps
|
|
456
|
+
*
|
|
457
|
+
* @throws Will throw an error if a group with the same name already exists
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* ```typescript
|
|
461
|
+
* const group = await model.create({
|
|
462
|
+
* name: "billing_redesign",
|
|
463
|
+
* note: "All flags related to the new billing flow",
|
|
464
|
+
* });
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
async create(input) {
|
|
468
|
+
const cols = ["name"];
|
|
469
|
+
const vals = [input.name];
|
|
470
|
+
if ("note" in input) {
|
|
471
|
+
cols.push("note");
|
|
472
|
+
vals.push(input.note ?? null);
|
|
473
|
+
}
|
|
474
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
475
|
+
const sql = `INSERT INTO ${GROUP_TABLE} (${cols.join(", ")})
|
|
476
|
+
VALUES (${placeholders})
|
|
477
|
+
RETURNING ${GROUP_COLUMNS.join(", ")}`;
|
|
478
|
+
const res = await this.db.query(sql, vals);
|
|
479
|
+
return mapGroupRow(res.rows[0]);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Retrieves a feature flag group by its ID.
|
|
483
|
+
*
|
|
484
|
+
* @param id - The group ID
|
|
485
|
+
* @returns The group if found, null otherwise
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* ```typescript
|
|
489
|
+
* const group = await model.getById(1);
|
|
490
|
+
* if (group) {
|
|
491
|
+
* console.log(group.name);
|
|
492
|
+
* }
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
async getById(id) {
|
|
496
|
+
const sql = `SELECT ${GROUP_COLUMNS.join(", ")} FROM ${GROUP_TABLE} WHERE id = $1`;
|
|
497
|
+
const res = await this.db.query(sql, [id]);
|
|
498
|
+
return res.rows.length > 0 ? mapGroupRow(res.rows[0]) : null;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Retrieves a feature flag group by its name.
|
|
502
|
+
*
|
|
503
|
+
* @param name - The group name
|
|
504
|
+
* @returns The group if found, null otherwise
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```typescript
|
|
508
|
+
* const group = await model.getByName("billing_redesign");
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
511
|
+
async getByName(name) {
|
|
512
|
+
const sql = `SELECT ${GROUP_COLUMNS.join(", ")} FROM ${GROUP_TABLE} WHERE name = $1`;
|
|
513
|
+
const res = await this.db.query(sql, [name]);
|
|
514
|
+
return res.rows.length > 0 ? mapGroupRow(res.rows[0]) : null;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Lists all feature flag groups.
|
|
518
|
+
*
|
|
519
|
+
* @returns Array of all groups
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* ```typescript
|
|
523
|
+
* const groups = await model.list();
|
|
524
|
+
* for (const group of groups) {
|
|
525
|
+
* console.log(group.name);
|
|
526
|
+
* }
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
529
|
+
async list() {
|
|
530
|
+
const sql = `SELECT ${GROUP_COLUMNS.join(", ")} FROM ${GROUP_TABLE} ORDER BY created DESC`;
|
|
531
|
+
const res = await this.db.query(sql);
|
|
532
|
+
return res.rows.map(mapGroupRow);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Updates a feature flag group.
|
|
536
|
+
*
|
|
537
|
+
* @param id - The group ID to update
|
|
538
|
+
* @param changes - Fields to update
|
|
539
|
+
* @returns The updated group if found, null otherwise
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* ```typescript
|
|
543
|
+
* const updated = await model.update(1, {
|
|
544
|
+
* note: "Updated description",
|
|
545
|
+
* });
|
|
546
|
+
* ```
|
|
547
|
+
*/
|
|
548
|
+
async update(id, changes) {
|
|
549
|
+
const updates = [];
|
|
550
|
+
const vals = [];
|
|
551
|
+
let paramIdx = 1;
|
|
552
|
+
if ("name" in changes) {
|
|
553
|
+
updates.push(`name = $${paramIdx++}`);
|
|
554
|
+
vals.push(changes.name);
|
|
555
|
+
}
|
|
556
|
+
if ("note" in changes) {
|
|
557
|
+
updates.push(`note = $${paramIdx++}`);
|
|
558
|
+
vals.push(changes.note ?? null);
|
|
559
|
+
}
|
|
560
|
+
if (updates.length === 0) {
|
|
561
|
+
return this.getById(id);
|
|
562
|
+
}
|
|
563
|
+
vals.push(id);
|
|
564
|
+
const sql = `UPDATE ${GROUP_TABLE}
|
|
565
|
+
SET ${updates.join(", ")}
|
|
566
|
+
WHERE id = $${paramIdx}
|
|
567
|
+
RETURNING ${GROUP_COLUMNS.join(", ")}`;
|
|
568
|
+
const res = await this.db.query(sql, vals);
|
|
569
|
+
return res.rows.length > 0 ? mapGroupRow(res.rows[0]) : null;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Deletes a feature flag group and all its member relationships.
|
|
573
|
+
*
|
|
574
|
+
* @param id - The group ID to delete
|
|
575
|
+
* @returns true if deleted, false if not found
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* ```typescript
|
|
579
|
+
* const deleted = await model.delete(1);
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
582
|
+
async delete(id) {
|
|
583
|
+
const res = await this.db.query(`DELETE FROM ${GROUP_TABLE} WHERE id = $1`, [id]);
|
|
584
|
+
return res.rowCount > 0;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Adds a feature flag to a group.
|
|
588
|
+
*
|
|
589
|
+
* @param groupId - The group ID
|
|
590
|
+
* @param featureFlagId - The feature flag ID to add
|
|
591
|
+
* @returns true if added successfully, false if already exists
|
|
592
|
+
*
|
|
593
|
+
* @throws Will throw an error if group or feature flag doesn't exist
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```typescript
|
|
597
|
+
* await model.addFeatureFlag(1, 5);
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
async addFeatureFlag(groupId, featureFlagId) {
|
|
601
|
+
try {
|
|
602
|
+
const sql = `INSERT INTO ${GROUP_MEMBER_TABLE} (group_id, feature_flag_id)
|
|
603
|
+
VALUES ($1, $2)`;
|
|
604
|
+
await this.db.query(sql, [groupId, featureFlagId]);
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
catch (err) {
|
|
608
|
+
// If unique constraint violation, return false
|
|
609
|
+
if (err.code === "23505") {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Removes a feature flag from a group.
|
|
617
|
+
*
|
|
618
|
+
* @param groupId - The group ID
|
|
619
|
+
* @param featureFlagId - The feature flag ID to remove
|
|
620
|
+
* @returns true if removed, false if relationship didn't exist
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* ```typescript
|
|
624
|
+
* await model.removeFeatureFlag(1, 5);
|
|
625
|
+
* ```
|
|
626
|
+
*/
|
|
627
|
+
async removeFeatureFlag(groupId, featureFlagId) {
|
|
628
|
+
const sql = `DELETE FROM ${GROUP_MEMBER_TABLE}
|
|
629
|
+
WHERE group_id = $1 AND feature_flag_id = $2`;
|
|
630
|
+
const res = await this.db.query(sql, [groupId, featureFlagId]);
|
|
631
|
+
return res.rowCount > 0;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Gets all feature flags in a group.
|
|
635
|
+
*
|
|
636
|
+
* @param groupId - The group ID
|
|
637
|
+
* @returns Array of feature flags in the group
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* ```typescript
|
|
641
|
+
* const flags = await model.getFeatureFlags(1);
|
|
642
|
+
* for (const flag of flags) {
|
|
643
|
+
* console.log(flag.name);
|
|
644
|
+
* }
|
|
645
|
+
* ```
|
|
646
|
+
*/
|
|
647
|
+
async getFeatureFlags(groupId) {
|
|
648
|
+
const sql = `
|
|
649
|
+
SELECT ${COLUMNS.map((c) => `f.${c}`).join(", ")}
|
|
650
|
+
FROM ${TABLE} f
|
|
651
|
+
INNER JOIN ${GROUP_MEMBER_TABLE} gm ON f.id = gm.feature_flag_id
|
|
652
|
+
WHERE gm.group_id = $1
|
|
653
|
+
ORDER BY f.created DESC
|
|
654
|
+
`;
|
|
655
|
+
const res = await this.db.query(sql, [groupId]);
|
|
656
|
+
return res.rows.map(mapRow);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Gets all groups that contain a specific feature flag.
|
|
660
|
+
*
|
|
661
|
+
* @param featureFlagId - The feature flag ID
|
|
662
|
+
* @returns Array of groups containing the feature flag
|
|
663
|
+
*
|
|
664
|
+
* @example
|
|
665
|
+
* ```typescript
|
|
666
|
+
* const groups = await model.getGroupsForFeatureFlag(5);
|
|
667
|
+
* ```
|
|
668
|
+
*/
|
|
669
|
+
async getGroupsForFeatureFlag(featureFlagId) {
|
|
670
|
+
const sql = `
|
|
671
|
+
SELECT ${GROUP_COLUMNS.map((c) => `g.${c}`).join(", ")}
|
|
672
|
+
FROM ${GROUP_TABLE} g
|
|
673
|
+
INNER JOIN ${GROUP_MEMBER_TABLE} gm ON g.id = gm.group_id
|
|
674
|
+
WHERE gm.feature_flag_id = $1
|
|
675
|
+
ORDER BY g.created DESC
|
|
676
|
+
`;
|
|
677
|
+
const res = await this.db.query(sql, [featureFlagId]);
|
|
678
|
+
return res.rows.map(mapGroupRow);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Updates all feature flags in a group with the same changes.
|
|
682
|
+
*
|
|
683
|
+
* @param groupId - The group ID
|
|
684
|
+
* @param changes - Fields to update (only everyone, percent, roles, groups, users allowed)
|
|
685
|
+
* @returns The number of feature flags updated
|
|
686
|
+
*
|
|
687
|
+
* @example
|
|
688
|
+
* ```typescript
|
|
689
|
+
* // Enable all flags in a group for everyone
|
|
690
|
+
* const count = await model.updateAll(1, { everyone: true });
|
|
691
|
+
*
|
|
692
|
+
* // Set percentage rollout for all flags in a group
|
|
693
|
+
* const count = await model.updateAll(1, { percent: 25 });
|
|
694
|
+
*
|
|
695
|
+
* // Add roles to all flags in a group
|
|
696
|
+
* const count = await model.updateAll(1, { roles: ["admin", "beta"] });
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
async updateAll(groupId, changes) {
|
|
700
|
+
const updates = [];
|
|
701
|
+
const vals = [];
|
|
702
|
+
let paramIdx = 1;
|
|
703
|
+
if ("everyone" in changes) {
|
|
704
|
+
updates.push(`everyone = $${paramIdx++}`);
|
|
705
|
+
vals.push(changes.everyone ?? null);
|
|
706
|
+
}
|
|
707
|
+
if ("percent" in changes) {
|
|
708
|
+
updates.push(`percent = $${paramIdx++}`);
|
|
709
|
+
vals.push(changes.percent ?? null);
|
|
710
|
+
}
|
|
711
|
+
if ("roles" in changes) {
|
|
712
|
+
updates.push(`roles = $${paramIdx++}`);
|
|
713
|
+
vals.push(changes.roles ?? null);
|
|
714
|
+
}
|
|
715
|
+
if ("groups" in changes) {
|
|
716
|
+
updates.push(`groups = $${paramIdx++}`);
|
|
717
|
+
vals.push(changes.groups ?? null);
|
|
718
|
+
}
|
|
719
|
+
if ("users" in changes) {
|
|
720
|
+
updates.push(`users = $${paramIdx++}`);
|
|
721
|
+
vals.push(changes.users ?? null);
|
|
722
|
+
}
|
|
723
|
+
if (updates.length === 0) {
|
|
724
|
+
return 0;
|
|
725
|
+
}
|
|
726
|
+
vals.push(groupId);
|
|
727
|
+
const sql = `
|
|
728
|
+
UPDATE ${TABLE}
|
|
729
|
+
SET ${updates.join(", ")}
|
|
730
|
+
WHERE id IN (
|
|
731
|
+
SELECT feature_flag_id
|
|
732
|
+
FROM ${GROUP_MEMBER_TABLE}
|
|
733
|
+
WHERE group_id = $${paramIdx}
|
|
734
|
+
)
|
|
735
|
+
`;
|
|
736
|
+
const res = await this.db.query(sql, vals);
|
|
737
|
+
return res.rowCount;
|
|
738
|
+
}
|
|
739
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -42,4 +42,19 @@ export type ExpiredFeatureFlagEventHandler = ({ flag, }: {
|
|
|
42
42
|
export type FeatureFlagEventHandlers = {
|
|
43
43
|
onExpired?: ExpiredFeatureFlagEventHandler;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Represents a feature flag group used to manage multiple feature flags together.
|
|
47
|
+
*/
|
|
48
|
+
export type FeatureFlagGroup = {
|
|
49
|
+
/** Unique identifier for the feature flag group */
|
|
50
|
+
id: number;
|
|
51
|
+
/** Human readable name of the feature flag group */
|
|
52
|
+
name: string;
|
|
53
|
+
/** Description of what this group is for */
|
|
54
|
+
note?: string;
|
|
55
|
+
/** Date when the group was created */
|
|
56
|
+
created: Date;
|
|
57
|
+
/** Date when the group was last modified */
|
|
58
|
+
modified: Date;
|
|
59
|
+
};
|
|
45
60
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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"}
|
|
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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
// Create the flapjack_feature_flag_group table
|
|
13
|
+
pgm.createTable("flapjack_feature_flag_group", {
|
|
14
|
+
id: "id",
|
|
15
|
+
name: { type: "text", notNull: true, unique: true },
|
|
16
|
+
note: { type: "text" },
|
|
17
|
+
created: { type: "timestamptz", notNull: true, default: pgm.func("now()") },
|
|
18
|
+
modified: {
|
|
19
|
+
type: "timestamptz",
|
|
20
|
+
notNull: true,
|
|
21
|
+
default: pgm.func("now()"),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Create the flapjack_feature_flag_group_member relation table
|
|
26
|
+
pgm.createTable("flapjack_feature_flag_group_member", {
|
|
27
|
+
id: "id",
|
|
28
|
+
group_id: {
|
|
29
|
+
type: "integer",
|
|
30
|
+
notNull: true,
|
|
31
|
+
references: "flapjack_feature_flag_group",
|
|
32
|
+
onDelete: "CASCADE",
|
|
33
|
+
},
|
|
34
|
+
feature_flag_id: {
|
|
35
|
+
type: "integer",
|
|
36
|
+
notNull: true,
|
|
37
|
+
references: "flapjack_feature_flag",
|
|
38
|
+
onDelete: "CASCADE",
|
|
39
|
+
},
|
|
40
|
+
created: { type: "timestamptz", notNull: true, default: pgm.func("now()") },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Add unique constraint on group_id and feature_flag_id
|
|
44
|
+
pgm.addConstraint(
|
|
45
|
+
"flapjack_feature_flag_group_member",
|
|
46
|
+
"unique_group_feature_flag",
|
|
47
|
+
{
|
|
48
|
+
unique: ["group_id", "feature_flag_id"],
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Create indexes for efficient lookups
|
|
53
|
+
pgm.createIndex("flapjack_feature_flag_group_member", "group_id");
|
|
54
|
+
pgm.createIndex("flapjack_feature_flag_group_member", "feature_flag_id");
|
|
55
|
+
|
|
56
|
+
// Attach the modified timestamp trigger to the group table
|
|
57
|
+
pgm.sql(`
|
|
58
|
+
CREATE TRIGGER flapjack_feature_flag_group_set_modified
|
|
59
|
+
BEFORE UPDATE ON flapjack_feature_flag_group
|
|
60
|
+
FOR EACH ROW
|
|
61
|
+
EXECUTE FUNCTION flapjack_set_modified_timestamp();
|
|
62
|
+
`);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
|
67
|
+
* @param run {() => void | undefined}
|
|
68
|
+
* @returns {Promise<void> | void}
|
|
69
|
+
*/
|
|
70
|
+
export const down = (pgm) => {
|
|
71
|
+
pgm.sql(`
|
|
72
|
+
DROP TRIGGER IF EXISTS flapjack_feature_flag_group_set_modified
|
|
73
|
+
ON flapjack_feature_flag_group;
|
|
74
|
+
`);
|
|
75
|
+
pgm.dropTable("flapjack_feature_flag_group_member");
|
|
76
|
+
pgm.dropTable("flapjack_feature_flag_group");
|
|
77
|
+
};
|