@brandtg/flapjack 0.1.2 → 1.0.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
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,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
@@ -1,3 +1,3 @@
1
- export { FeatureFlagModel } from "./model.js";
1
+ export { FeatureFlagModel, FeatureFlagGroupModel } from "./model.js";
2
2
  export { FeatureFlagCache, InMemoryCache } from "./cache.js";
3
3
  export { runMigrations } from "./migrate.js";
package/dist/migrate.d.ts CHANGED
@@ -4,21 +4,16 @@
4
4
  export interface MigrationOptions {
5
5
  /** PostgreSQL connection URL (e.g., "postgresql://user:pass@localhost/dbname") */
6
6
  databaseUrl: string;
7
- /** Name of the migrations tracking table (default: 'pgmigrations') */
8
- migrationsTable?: string;
9
- /** Schema name to create tables in (optional) */
10
- schema?: string;
11
7
  }
12
8
  /**
13
9
  * Run Flapjack database migrations.
14
10
  *
15
11
  * This function applies all pending migrations to create or update the
16
- * flapjack_feature_flag table in your PostgreSQL database.
12
+ * flapjack.feature_flag table in your PostgreSQL database. All tables,
13
+ * including the migrations tracking table, are created in the flapjack schema.
17
14
  *
18
15
  * @param options - Migration configuration options
19
16
  * @param options.databaseUrl - PostgreSQL connection URL
20
- * @param options.migrationsTable - Name of the migrations tracking table (default: 'pgmigrations')
21
- * @param options.schema - Schema name to create tables in (optional)
22
17
  * @returns Promise that resolves when migrations are complete
23
18
  *
24
19
  * @throws Will throw an error if the database connection fails or migrations cannot be applied
@@ -27,22 +22,9 @@ export interface MigrationOptions {
27
22
  * ```typescript
28
23
  * import { runMigrations } from "@brandtg/flapjack";
29
24
  *
30
- * // Basic usage
31
25
  * await runMigrations({
32
26
  * databaseUrl: process.env.DATABASE_URL,
33
27
  * });
34
- *
35
- * // With custom migrations table
36
- * await runMigrations({
37
- * databaseUrl: process.env.DATABASE_URL,
38
- * migrationsTable: "my_migrations",
39
- * });
40
- *
41
- * // With custom schema
42
- * await runMigrations({
43
- * databaseUrl: process.env.DATABASE_URL,
44
- * schema: "feature_flags",
45
- * });
46
28
  * ```
47
29
  */
48
30
  export declare function runMigrations(options: MigrationOptions): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB5E"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB5E"}
package/dist/migrate.js CHANGED
@@ -7,12 +7,11 @@ const __dirname = path.dirname(__filename);
7
7
  * Run Flapjack database migrations.
8
8
  *
9
9
  * This function applies all pending migrations to create or update the
10
- * flapjack_feature_flag table in your PostgreSQL database.
10
+ * flapjack.feature_flag table in your PostgreSQL database. All tables,
11
+ * including the migrations tracking table, are created in the flapjack schema.
11
12
  *
12
13
  * @param options - Migration configuration options
13
14
  * @param options.databaseUrl - PostgreSQL connection URL
14
- * @param options.migrationsTable - Name of the migrations tracking table (default: 'pgmigrations')
15
- * @param options.schema - Schema name to create tables in (optional)
16
15
  * @returns Promise that resolves when migrations are complete
17
16
  *
18
17
  * @throws Will throw an error if the database connection fails or migrations cannot be applied
@@ -21,34 +20,22 @@ const __dirname = path.dirname(__filename);
21
20
  * ```typescript
22
21
  * import { runMigrations } from "@brandtg/flapjack";
23
22
  *
24
- * // Basic usage
25
23
  * await runMigrations({
26
24
  * databaseUrl: process.env.DATABASE_URL,
27
25
  * });
28
- *
29
- * // With custom migrations table
30
- * await runMigrations({
31
- * databaseUrl: process.env.DATABASE_URL,
32
- * migrationsTable: "my_migrations",
33
- * });
34
- *
35
- * // With custom schema
36
- * await runMigrations({
37
- * databaseUrl: process.env.DATABASE_URL,
38
- * schema: "feature_flags",
39
- * });
40
26
  * ```
41
27
  */
42
28
  export async function runMigrations(options) {
43
- const { databaseUrl, migrationsTable = "pgmigrations", schema } = options;
29
+ const { databaseUrl } = options;
44
30
  // Path to migrations directory (relative to dist in production)
45
31
  const migrationsDir = path.resolve(__dirname, "../migrations");
46
32
  const migrationConfig = {
47
33
  databaseUrl,
48
34
  dir: migrationsDir,
49
35
  direction: "up",
50
- migrationsTable,
51
- ...(schema && { schema }),
36
+ schema: "flapjack",
37
+ migrationsTable: "pgmigrations",
38
+ createSchema: true,
52
39
  verbose: false,
53
40
  };
54
41
  await runner(migrationConfig);
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
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,IAAI,CAAC;AAItC,UAAU,SAAS;IACjB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CAC/D;AAkBD,KAAK,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,SAAS,GAAG,UAAU,CAAC,CAAC;AACpE,KAAK,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC;AA0B/E;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,EAAE,CAAY;IACtB,OAAO,CAAC,aAAa,CAAC,CAA2B;IAEjD;;;;;;;;;;;OAWG;gBACS,EAAE,EAAE,SAAS,EAAE,aAAa,CAAC,EAAE,wBAAwB;IAKnE;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACG,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAuCtD;;;;;;;;;;;;;OAaG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAOtD;;;;;;;;;;;;;OAaG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAO1D;;;;;;;;;;;;;OAaG;IACG,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAMpC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,MAAM,CACV,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAiD9B;;;;;;;;;;;;;OAaG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK1C;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;;;;;;;;;;;;;;;;;OAkBG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACG,eAAe,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,MAAM,GACP,EAAE;QACD,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,GAAG,OAAO,CAAC,OAAO,CAAC;CAyDrB"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
package/dist/model.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import MurmurHash3 from "imurmurhash";
2
- const TABLE = "flapjack_feature_flag";
2
+ const TABLE = "flapjack.feature_flag";
3
3
  const COLUMNS = [
4
4
  "id",
5
5
  "name",
@@ -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
@@ -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"}
@@ -9,30 +9,37 @@ export const shorthands = undefined;
9
9
  * @returns {Promise<void> | void}
10
10
  */
11
11
  export const up = (pgm) => {
12
- // Create the flapjack_feature_flag table
13
- pgm.createTable("flapjack_feature_flag", {
14
- id: "id",
15
- name: { type: "text", notNull: true, unique: true },
16
- everyone: { type: "boolean" },
17
- percent: {
18
- type: "numeric(3,1)",
19
- check: "percent >= 0 AND percent <= 99.9",
12
+ // Create the feature_flag table (schema created by node-pg-migrate)
13
+ pgm.createTable(
14
+ { schema: "flapjack", name: "feature_flag" },
15
+ {
16
+ id: "id",
17
+ name: { type: "text", notNull: true, unique: true },
18
+ everyone: { type: "boolean" },
19
+ percent: {
20
+ type: "numeric(3,1)",
21
+ check: "percent >= 0 AND percent <= 99.9",
22
+ },
23
+ roles: { type: "text[]", default: pgm.func("'{}'::text[]") },
24
+ groups: { type: "text[]", default: pgm.func("'{}'::text[]") },
25
+ users: { type: "text[]", default: pgm.func("'{}'::text[]") },
26
+ note: { type: "text" },
27
+ created: {
28
+ type: "timestamptz",
29
+ notNull: true,
30
+ default: pgm.func("now()"),
31
+ },
32
+ modified: {
33
+ type: "timestamptz",
34
+ notNull: true,
35
+ default: pgm.func("now()"),
36
+ },
20
37
  },
21
- roles: { type: "text[]", default: pgm.func("'{}'::text[]") },
22
- groups: { type: "text[]", default: pgm.func("'{}'::text[]") },
23
- users: { type: "text[]", default: pgm.func("'{}'::text[]") },
24
- note: { type: "text" },
25
- created: { type: "timestamptz", notNull: true, default: pgm.func("now()") },
26
- modified: {
27
- type: "timestamptz",
28
- notNull: true,
29
- default: pgm.func("now()"),
30
- },
31
- });
38
+ );
32
39
 
33
40
  // Create a trigger to update the modified timestamp on row update
34
41
  pgm.sql(`
35
- CREATE OR REPLACE FUNCTION flapjack_set_modified_timestamp()
42
+ CREATE OR REPLACE FUNCTION flapjack.set_modified_timestamp()
36
43
  RETURNS trigger AS $$
37
44
  BEGIN
38
45
  NEW.modified = now();
@@ -41,12 +48,12 @@ export const up = (pgm) => {
41
48
  $$ LANGUAGE plpgsql;
42
49
  `);
43
50
 
44
- // Attach the trigger to the flapjack_feature_flag table
51
+ // Attach the trigger to the feature_flag table
45
52
  pgm.sql(`
46
- CREATE TRIGGER flapjack_feature_flag_set_modified
47
- BEFORE UPDATE ON flapjack_feature_flag
53
+ CREATE TRIGGER feature_flag_set_modified
54
+ BEFORE UPDATE ON flapjack.feature_flag
48
55
  FOR EACH ROW
49
- EXECUTE FUNCTION flapjack_set_modified_timestamp();
56
+ EXECUTE FUNCTION flapjack.set_modified_timestamp();
50
57
  `);
51
58
  };
52
59
 
@@ -57,9 +64,9 @@ export const up = (pgm) => {
57
64
  */
58
65
  export const down = (pgm) => {
59
66
  pgm.sql(`
60
- DROP TRIGGER IF EXISTS flapjack_feature_flag_set_modified
61
- ON flapjack_feature_flag;
67
+ DROP TRIGGER IF EXISTS feature_flag_set_modified
68
+ ON flapjack.feature_flag;
62
69
  `);
63
- pgm.sql(`DROP FUNCTION IF EXISTS flapjack_set_modified_timestamp;`);
64
- pgm.dropTable("flapjack_feature_flag");
70
+ pgm.sql(`DROP FUNCTION IF EXISTS flapjack.set_modified_timestamp;`);
71
+ pgm.dropTable({ schema: "flapjack", name: "feature_flag" });
65
72
  };
@@ -9,9 +9,12 @@ export const shorthands = undefined;
9
9
  * @returns {Promise<void> | void}
10
10
  */
11
11
  export const up = (pgm) => {
12
- pgm.addColumn("flapjack_feature_flag", {
13
- expires: { type: "timestamptz" },
14
- });
12
+ pgm.addColumn(
13
+ { schema: "flapjack", name: "feature_flag" },
14
+ {
15
+ expires: { type: "timestamptz" },
16
+ },
17
+ );
15
18
  };
16
19
 
17
20
  /**
@@ -20,5 +23,5 @@ export const up = (pgm) => {
20
23
  * @returns {Promise<void> | void}
21
24
  */
22
25
  export const down = (pgm) => {
23
- pgm.dropColumn("flapjack_feature_flag", "expires");
26
+ pgm.dropColumn({ schema: "flapjack", name: "feature_flag" }, "expires");
24
27
  };
@@ -0,0 +1,97 @@
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 feature_flag_group table
13
+ pgm.createTable(
14
+ { schema: "flapjack", name: "feature_flag_group" },
15
+ {
16
+ id: "id",
17
+ name: { type: "text", notNull: true, unique: true },
18
+ note: { type: "text" },
19
+ created: {
20
+ type: "timestamptz",
21
+ notNull: true,
22
+ default: pgm.func("now()"),
23
+ },
24
+ modified: {
25
+ type: "timestamptz",
26
+ notNull: true,
27
+ default: pgm.func("now()"),
28
+ },
29
+ },
30
+ );
31
+
32
+ // Create the feature_flag_group_member relation table
33
+ pgm.createTable(
34
+ { schema: "flapjack", name: "feature_flag_group_member" },
35
+ {
36
+ id: "id",
37
+ group_id: {
38
+ type: "integer",
39
+ notNull: true,
40
+ references: { schema: "flapjack", name: "feature_flag_group" },
41
+ onDelete: "CASCADE",
42
+ },
43
+ feature_flag_id: {
44
+ type: "integer",
45
+ notNull: true,
46
+ references: { schema: "flapjack", name: "feature_flag" },
47
+ onDelete: "CASCADE",
48
+ },
49
+ created: {
50
+ type: "timestamptz",
51
+ notNull: true,
52
+ default: pgm.func("now()"),
53
+ },
54
+ },
55
+ );
56
+
57
+ // Add unique constraint on group_id and feature_flag_id
58
+ pgm.addConstraint(
59
+ { schema: "flapjack", name: "feature_flag_group_member" },
60
+ "unique_group_feature_flag",
61
+ {
62
+ unique: ["group_id", "feature_flag_id"],
63
+ },
64
+ );
65
+
66
+ // Create indexes for efficient lookups
67
+ pgm.createIndex(
68
+ { schema: "flapjack", name: "feature_flag_group_member" },
69
+ "group_id",
70
+ );
71
+ pgm.createIndex(
72
+ { schema: "flapjack", name: "feature_flag_group_member" },
73
+ "feature_flag_id",
74
+ );
75
+
76
+ // Attach the modified timestamp trigger to the group table
77
+ pgm.sql(`
78
+ CREATE TRIGGER feature_flag_group_set_modified
79
+ BEFORE UPDATE ON flapjack.feature_flag_group
80
+ FOR EACH ROW
81
+ EXECUTE FUNCTION flapjack.set_modified_timestamp();
82
+ `);
83
+ };
84
+
85
+ /**
86
+ * @param pgm {import('node-pg-migrate').MigrationBuilder}
87
+ * @param run {() => void | undefined}
88
+ * @returns {Promise<void> | void}
89
+ */
90
+ export const down = (pgm) => {
91
+ pgm.sql(`
92
+ DROP TRIGGER IF EXISTS feature_flag_group_set_modified
93
+ ON flapjack.feature_flag_group;
94
+ `);
95
+ pgm.dropTable({ schema: "flapjack", name: "feature_flag_group_member" });
96
+ pgm.dropTable({ schema: "flapjack", name: "feature_flag_group" });
97
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandtg/flapjack",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "description": "A simple feature flags library with PostgreSQL integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",