@flaghoist/adapter-postgres 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 CHANGED
@@ -28,6 +28,11 @@ await pool.query(postgresSchema())
28
28
 
29
29
  Pass a name if you want it somewhere other than `flaghoist_flags`.
30
30
 
31
+ Webhook endpoints (if you use them) live in a second table, `flaghoist_webhooks` by default.
32
+ `postgresWebhookSchema()` gives you that table's SQL the same way, and `postgresAdapter(pool, {
33
+ webhookTable: '...' })` renames it. `initPostgres(pool)` creates every table the adapter uses in one call, if you'd
34
+ rather not run the statements by hand.
35
+
31
36
  This is the one to choose when your flags should live in a database you already back up, or when the
32
37
  database sits inside a VPC that a Worker cannot reach anyway. Note that it wants a real TCP
33
38
  connection, so it suits Node, Bun or a container rather than an edge runtime.
package/dist/index.cjs CHANGED
@@ -22,7 +22,9 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  initPostgres: () => initPostgres,
24
24
  postgresAdapter: () => postgresAdapter,
25
- postgresSchema: () => postgresSchema
25
+ postgresRecordSchema: () => postgresRecordSchema,
26
+ postgresSchema: () => postgresSchema,
27
+ postgresWebhookSchema: () => postgresWebhookSchema
26
28
  });
27
29
  module.exports = __toCommonJS(index_exports);
28
30
  var import_core = require("@flaghoist/core");
@@ -38,8 +40,31 @@ function assertIdentifier(name) {
38
40
  function postgresSchema(table = "flaghoist_flags") {
39
41
  return `CREATE TABLE IF NOT EXISTS ${assertIdentifier(table)} (key text PRIMARY KEY, value jsonb NOT NULL)`;
40
42
  }
41
- async function initPostgres(client, table = "flaghoist_flags") {
43
+ function postgresWebhookSchema(table = "flaghoist_webhooks") {
44
+ return `CREATE TABLE IF NOT EXISTS ${assertIdentifier(table)} (id text PRIMARY KEY, value jsonb NOT NULL)`;
45
+ }
46
+ function postgresRecordSchema(table = "flaghoist_records") {
47
+ return `CREATE TABLE IF NOT EXISTS ${assertIdentifier(table)} (collection text NOT NULL, id text NOT NULL, value jsonb NOT NULL, PRIMARY KEY (collection, id))`;
48
+ }
49
+ async function initPostgres(client, table = "flaghoist_flags", webhookTable = "flaghoist_webhooks", recordTable = "flaghoist_records") {
42
50
  await client.query(postgresSchema(table));
51
+ await client.query(postgresWebhookSchema(webhookTable));
52
+ await client.query(postgresRecordSchema(recordTable));
53
+ }
54
+ var wrapRecord = (value) => JSON.stringify({ flaghoistRecord: 1, value });
55
+ function unwrapRecord(raw) {
56
+ let parsed = raw;
57
+ if (typeof raw === "string") {
58
+ try {
59
+ parsed = JSON.parse(raw);
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ if (parsed === null || typeof parsed !== "object") return null;
65
+ const envelope = parsed;
66
+ if (envelope.flaghoistRecord !== 1) return null;
67
+ return envelope.value ?? null;
43
68
  }
44
69
  function toFlag(value) {
45
70
  if (value == null) return null;
@@ -54,6 +79,8 @@ function toFlag(value) {
54
79
  }
55
80
  function postgresAdapter(client, options = {}) {
56
81
  const table = assertIdentifier(options.table ?? "flaghoist_flags");
82
+ const whTable = assertIdentifier(options.webhookTable ?? "flaghoist_webhooks");
83
+ const recTable = assertIdentifier(options.recordTable ?? "flaghoist_records");
57
84
  return {
58
85
  async get(key) {
59
86
  const { rows } = await client.query(`SELECT value FROM ${table} WHERE key = $1`, [key]);
@@ -78,6 +105,66 @@ function postgresAdapter(client, options = {}) {
78
105
  if (flag) flags.push(flag);
79
106
  }
80
107
  return flags;
108
+ },
109
+ async putWebhook(id, webhook) {
110
+ await client.query(
111
+ `INSERT INTO ${whTable} (id, value) VALUES ($1, $2::jsonb)
112
+ ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value`,
113
+ [id, JSON.stringify(webhook)]
114
+ );
115
+ },
116
+ async getWebhook(id) {
117
+ const { rows } = await client.query(`SELECT value FROM ${whTable} WHERE id = $1`, [id]);
118
+ const row = rows[0];
119
+ if (!row) return null;
120
+ return typeof row.value === "string" ? JSON.parse(row.value) : row.value;
121
+ },
122
+ async deleteWebhook(id) {
123
+ await client.query(`DELETE FROM ${whTable} WHERE id = $1`, [id]);
124
+ },
125
+ async listWebhooks() {
126
+ const { rows } = await client.query(`SELECT value FROM ${whTable}`);
127
+ return rows.map((row) => {
128
+ const v = row.value;
129
+ return typeof v === "string" ? JSON.parse(v) : v;
130
+ });
131
+ },
132
+ async getRecord(collection, id) {
133
+ (0, import_core.assertRecordAddress)(collection, id);
134
+ const { rows } = await client.query(
135
+ `SELECT value FROM ${recTable} WHERE collection = $1 AND id = $2`,
136
+ [collection, id]
137
+ );
138
+ const row = rows[0];
139
+ return row ? unwrapRecord(row.value) : null;
140
+ },
141
+ async putRecord(collection, id, value) {
142
+ (0, import_core.assertRecordAddress)(collection, id);
143
+ await client.query(
144
+ `INSERT INTO ${recTable} (collection, id, value) VALUES ($1, $2, $3::jsonb)
145
+ ON CONFLICT (collection, id) DO UPDATE SET value = EXCLUDED.value`,
146
+ [collection, id, wrapRecord(value)]
147
+ );
148
+ },
149
+ async deleteRecord(collection, id) {
150
+ (0, import_core.assertRecordAddress)(collection, id);
151
+ await client.query(`DELETE FROM ${recTable} WHERE collection = $1 AND id = $2`, [
152
+ collection,
153
+ id
154
+ ]);
155
+ },
156
+ async listRecords(collection) {
157
+ (0, import_core.assertRecordAddress)(collection);
158
+ const { rows } = await client.query(
159
+ `SELECT id, value FROM ${recTable} WHERE collection = $1`,
160
+ [collection]
161
+ );
162
+ const entries = [];
163
+ for (const row of rows) {
164
+ const value = unwrapRecord(row.value);
165
+ if (value !== null) entries.push({ id: row.id, value });
166
+ }
167
+ return entries;
81
168
  }
82
169
  };
83
170
  }
@@ -85,5 +172,7 @@ function postgresAdapter(client, options = {}) {
85
172
  0 && (module.exports = {
86
173
  initPostgres,
87
174
  postgresAdapter,
88
- postgresSchema
175
+ postgresRecordSchema,
176
+ postgresSchema,
177
+ postgresWebhookSchema
89
178
  });
package/dist/index.d.cts CHANGED
@@ -12,11 +12,19 @@ interface PgQueryable {
12
12
  interface PostgresAdapterOptions {
13
13
  /** Table name. Must be a plain SQL identifier. Default: `"flaghoist_flags"`. */
14
14
  table?: string;
15
+ /** Webhook table name. Must be a plain SQL identifier. Default: `"flaghoist_webhooks"`. */
16
+ webhookTable?: string;
17
+ /** Record-store table name. Must be a plain SQL identifier. Default: `"flaghoist_records"`. */
18
+ recordTable?: string;
15
19
  }
16
20
  /** SQL that creates the flags table. Run once (or via `initPostgres`). */
17
21
  declare function postgresSchema(table?: string): string;
18
- /** Create the flags table if it does not already exist. */
19
- declare function initPostgres(client: PgQueryable, table?: string): Promise<void>;
22
+ /** SQL that creates the webhooks table. */
23
+ declare function postgresWebhookSchema(table?: string): string;
24
+ /** SQL that creates the record-store table. */
25
+ declare function postgresRecordSchema(table?: string): string;
26
+ /** Create the flags, webhooks and record-store tables if they do not already exist. */
27
+ declare function initPostgres(client: PgQueryable, table?: string, webhookTable?: string, recordTable?: string): Promise<void>;
20
28
  /**
21
29
  * A StorageAdapter backed by Postgres. Flags live in a `jsonb` table keyed by flag key. All
22
30
  * queries are parameterized; every read is re-validated through `parseFlag`, so corrupt rows
@@ -24,4 +32,4 @@ declare function initPostgres(client: PgQueryable, table?: string): Promise<void
24
32
  */
25
33
  declare function postgresAdapter(client: PgQueryable, options?: PostgresAdapterOptions): StorageAdapter;
26
34
 
27
- export { type PgQueryable, type PostgresAdapterOptions, initPostgres, postgresAdapter, postgresSchema };
35
+ export { type PgQueryable, type PostgresAdapterOptions, initPostgres, postgresAdapter, postgresRecordSchema, postgresSchema, postgresWebhookSchema };
package/dist/index.d.ts CHANGED
@@ -12,11 +12,19 @@ interface PgQueryable {
12
12
  interface PostgresAdapterOptions {
13
13
  /** Table name. Must be a plain SQL identifier. Default: `"flaghoist_flags"`. */
14
14
  table?: string;
15
+ /** Webhook table name. Must be a plain SQL identifier. Default: `"flaghoist_webhooks"`. */
16
+ webhookTable?: string;
17
+ /** Record-store table name. Must be a plain SQL identifier. Default: `"flaghoist_records"`. */
18
+ recordTable?: string;
15
19
  }
16
20
  /** SQL that creates the flags table. Run once (or via `initPostgres`). */
17
21
  declare function postgresSchema(table?: string): string;
18
- /** Create the flags table if it does not already exist. */
19
- declare function initPostgres(client: PgQueryable, table?: string): Promise<void>;
22
+ /** SQL that creates the webhooks table. */
23
+ declare function postgresWebhookSchema(table?: string): string;
24
+ /** SQL that creates the record-store table. */
25
+ declare function postgresRecordSchema(table?: string): string;
26
+ /** Create the flags, webhooks and record-store tables if they do not already exist. */
27
+ declare function initPostgres(client: PgQueryable, table?: string, webhookTable?: string, recordTable?: string): Promise<void>;
20
28
  /**
21
29
  * A StorageAdapter backed by Postgres. Flags live in a `jsonb` table keyed by flag key. All
22
30
  * queries are parameterized; every read is re-validated through `parseFlag`, so corrupt rows
@@ -24,4 +32,4 @@ declare function initPostgres(client: PgQueryable, table?: string): Promise<void
24
32
  */
25
33
  declare function postgresAdapter(client: PgQueryable, options?: PostgresAdapterOptions): StorageAdapter;
26
34
 
27
- export { type PgQueryable, type PostgresAdapterOptions, initPostgres, postgresAdapter, postgresSchema };
35
+ export { type PgQueryable, type PostgresAdapterOptions, initPostgres, postgresAdapter, postgresRecordSchema, postgresSchema, postgresWebhookSchema };
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  // src/index.ts
2
- import { parseFlag } from "@flaghoist/core";
2
+ import {
3
+ assertRecordAddress,
4
+ parseFlag
5
+ } from "@flaghoist/core";
3
6
  var IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
4
7
  function assertIdentifier(name) {
5
8
  if (!IDENTIFIER.test(name)) {
@@ -12,8 +15,31 @@ function assertIdentifier(name) {
12
15
  function postgresSchema(table = "flaghoist_flags") {
13
16
  return `CREATE TABLE IF NOT EXISTS ${assertIdentifier(table)} (key text PRIMARY KEY, value jsonb NOT NULL)`;
14
17
  }
15
- async function initPostgres(client, table = "flaghoist_flags") {
18
+ function postgresWebhookSchema(table = "flaghoist_webhooks") {
19
+ return `CREATE TABLE IF NOT EXISTS ${assertIdentifier(table)} (id text PRIMARY KEY, value jsonb NOT NULL)`;
20
+ }
21
+ function postgresRecordSchema(table = "flaghoist_records") {
22
+ return `CREATE TABLE IF NOT EXISTS ${assertIdentifier(table)} (collection text NOT NULL, id text NOT NULL, value jsonb NOT NULL, PRIMARY KEY (collection, id))`;
23
+ }
24
+ async function initPostgres(client, table = "flaghoist_flags", webhookTable = "flaghoist_webhooks", recordTable = "flaghoist_records") {
16
25
  await client.query(postgresSchema(table));
26
+ await client.query(postgresWebhookSchema(webhookTable));
27
+ await client.query(postgresRecordSchema(recordTable));
28
+ }
29
+ var wrapRecord = (value) => JSON.stringify({ flaghoistRecord: 1, value });
30
+ function unwrapRecord(raw) {
31
+ let parsed = raw;
32
+ if (typeof raw === "string") {
33
+ try {
34
+ parsed = JSON.parse(raw);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ if (parsed === null || typeof parsed !== "object") return null;
40
+ const envelope = parsed;
41
+ if (envelope.flaghoistRecord !== 1) return null;
42
+ return envelope.value ?? null;
17
43
  }
18
44
  function toFlag(value) {
19
45
  if (value == null) return null;
@@ -28,6 +54,8 @@ function toFlag(value) {
28
54
  }
29
55
  function postgresAdapter(client, options = {}) {
30
56
  const table = assertIdentifier(options.table ?? "flaghoist_flags");
57
+ const whTable = assertIdentifier(options.webhookTable ?? "flaghoist_webhooks");
58
+ const recTable = assertIdentifier(options.recordTable ?? "flaghoist_records");
31
59
  return {
32
60
  async get(key) {
33
61
  const { rows } = await client.query(`SELECT value FROM ${table} WHERE key = $1`, [key]);
@@ -52,11 +80,73 @@ function postgresAdapter(client, options = {}) {
52
80
  if (flag) flags.push(flag);
53
81
  }
54
82
  return flags;
83
+ },
84
+ async putWebhook(id, webhook) {
85
+ await client.query(
86
+ `INSERT INTO ${whTable} (id, value) VALUES ($1, $2::jsonb)
87
+ ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value`,
88
+ [id, JSON.stringify(webhook)]
89
+ );
90
+ },
91
+ async getWebhook(id) {
92
+ const { rows } = await client.query(`SELECT value FROM ${whTable} WHERE id = $1`, [id]);
93
+ const row = rows[0];
94
+ if (!row) return null;
95
+ return typeof row.value === "string" ? JSON.parse(row.value) : row.value;
96
+ },
97
+ async deleteWebhook(id) {
98
+ await client.query(`DELETE FROM ${whTable} WHERE id = $1`, [id]);
99
+ },
100
+ async listWebhooks() {
101
+ const { rows } = await client.query(`SELECT value FROM ${whTable}`);
102
+ return rows.map((row) => {
103
+ const v = row.value;
104
+ return typeof v === "string" ? JSON.parse(v) : v;
105
+ });
106
+ },
107
+ async getRecord(collection, id) {
108
+ assertRecordAddress(collection, id);
109
+ const { rows } = await client.query(
110
+ `SELECT value FROM ${recTable} WHERE collection = $1 AND id = $2`,
111
+ [collection, id]
112
+ );
113
+ const row = rows[0];
114
+ return row ? unwrapRecord(row.value) : null;
115
+ },
116
+ async putRecord(collection, id, value) {
117
+ assertRecordAddress(collection, id);
118
+ await client.query(
119
+ `INSERT INTO ${recTable} (collection, id, value) VALUES ($1, $2, $3::jsonb)
120
+ ON CONFLICT (collection, id) DO UPDATE SET value = EXCLUDED.value`,
121
+ [collection, id, wrapRecord(value)]
122
+ );
123
+ },
124
+ async deleteRecord(collection, id) {
125
+ assertRecordAddress(collection, id);
126
+ await client.query(`DELETE FROM ${recTable} WHERE collection = $1 AND id = $2`, [
127
+ collection,
128
+ id
129
+ ]);
130
+ },
131
+ async listRecords(collection) {
132
+ assertRecordAddress(collection);
133
+ const { rows } = await client.query(
134
+ `SELECT id, value FROM ${recTable} WHERE collection = $1`,
135
+ [collection]
136
+ );
137
+ const entries = [];
138
+ for (const row of rows) {
139
+ const value = unwrapRecord(row.value);
140
+ if (value !== null) entries.push({ id: row.id, value });
141
+ }
142
+ return entries;
55
143
  }
56
144
  };
57
145
  }
58
146
  export {
59
147
  initPostgres,
60
148
  postgresAdapter,
61
- postgresSchema
149
+ postgresRecordSchema,
150
+ postgresSchema,
151
+ postgresWebhookSchema
62
152
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaghoist/adapter-postgres",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Postgres StorageAdapter for Flaghoist — stores flags in a jsonb table via any node-postgres client.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "node": ">=20"
28
28
  },
29
29
  "dependencies": {
30
- "@flaghoist/core": "0.1.2"
30
+ "@flaghoist/core": "0.2.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "pg-mem": "^3.0.14",