@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 +5 -0
- package/dist/index.cjs +92 -3
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +93 -3
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
19
|
-
declare function
|
|
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
|
-
/**
|
|
19
|
-
declare function
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30
|
+
"@flaghoist/core": "0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"pg-mem": "^3.0.14",
|