@hasna/shortlinks 0.1.21 → 0.1.23
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 +14 -19
- package/dist/cli/index.js +392 -78
- package/dist/config.d.ts +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +332 -16
- package/dist/pg-store.d.ts +10 -2
- package/dist/pg-store.js +598 -0
- package/dist/runtime.d.ts +65 -0
- package/dist/runtime.js +181 -0
- package/dist/server.js +53 -5
- package/infra/aws-ec2-user-data.sh +32 -31
- package/package.json +15 -6
package/dist/config.d.ts
CHANGED
|
@@ -12,7 +12,9 @@ export interface ShortlinksConfig {
|
|
|
12
12
|
export declare function getDataDir(): string;
|
|
13
13
|
export declare function ensureDataDir(): string;
|
|
14
14
|
export declare function getConfigPath(): string;
|
|
15
|
+
export declare function getClickSaltPath(): string;
|
|
15
16
|
export declare function getDatabasePath(explicitPath?: string): string;
|
|
17
|
+
export declare function getClickSalt(): string;
|
|
16
18
|
export declare function loadConfig(): ShortlinksConfig;
|
|
17
19
|
export declare function saveConfig(config: ShortlinksConfig): void;
|
|
18
20
|
export declare function updateConfig(patch: ShortlinksConfig): ShortlinksConfig;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export { ShortlinksDatabase, SQLITE_MIGRATIONS, makeId, now } from "./database.js";
|
|
2
2
|
export { ShortlinksStore } from "./store.js";
|
|
3
|
-
export { PgShortlinksStore } from "./pg-store.js";
|
|
3
|
+
export { PgShortlinksStore, applyPostgresMigrations } from "./pg-store.js";
|
|
4
4
|
export { createShortlinksHandler, serveShortlinks } from "./server.js";
|
|
5
5
|
export { createCloudflarePlan, generateWorkerScript, writeWorkerFiles, upsertCloudflareDnsRecord } from "./cloudflare.js";
|
|
6
6
|
export { createLocalSetupPlan, registerMachinesDns } from "./local.js";
|
|
7
7
|
export { PG_MIGRATIONS } from "./pg-migrations.js";
|
|
8
|
+
export { CANONICAL_SHORTLINKS_POSTGRES_CLUSTER, CANONICAL_SHORTLINKS_POSTGRES_DATABASE, CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH, SHORTLINKS_RUNTIME_ENV, SHORTLINKS_RUNTIME_FALLBACK_ENV, assertShortlinksPostgresConfig, getCanonicalShortlinksPostgresConfig, getShortlinksDatabaseSsl, getShortlinksDatabaseUrl, getShortlinksRuntimeEnvName, getShortlinksRuntimeStatus, getShortlinksStoreMode, loadShortlinksRuntimeConfig, parseShortlinksStoreMode, redactDatabaseUrl, } from "./runtime.js";
|
|
8
9
|
export { formatShortUrl, getConfigPath, getDataDir, getDatabasePath, loadConfig, normalizeHostname, saveConfig } from "./config.js";
|
|
9
10
|
export { normalizeSlug, randomToken } from "./slug.js";
|
|
10
11
|
export type { AddDomainInput, Click, ClickInput, CreateLinkInput, Domain, Link, LinkStats } from "./types.js";
|
|
12
|
+
export type { CanonicalShortlinksPostgresConfig, RuntimeEnvStatus, ShortlinksPostgresConfig, ShortlinksRuntimeConfig, ShortlinksRuntimeEnv, ShortlinksRuntimeStatus, ShortlinksStoreMode, } from "./runtime.js";
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __require = import.meta.require;
|
|
3
|
-
|
|
4
2
|
// src/database.ts
|
|
5
3
|
import { Database } from "bun:sqlite";
|
|
6
4
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
7
5
|
import { dirname as dirname2 } from "path";
|
|
8
6
|
|
|
9
7
|
// src/config.ts
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { existsSync, linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
9
|
+
import { randomBytes } from "crypto";
|
|
11
10
|
import { homedir } from "os";
|
|
12
11
|
import { dirname, join, resolve } from "path";
|
|
13
12
|
var SERVICE_NAME = "shortlinks";
|
|
@@ -23,6 +22,9 @@ function ensureDataDir() {
|
|
|
23
22
|
function getConfigPath() {
|
|
24
23
|
return join(ensureDataDir(), "config.json");
|
|
25
24
|
}
|
|
25
|
+
function getClickSaltPath() {
|
|
26
|
+
return join(ensureDataDir(), "click-salt");
|
|
27
|
+
}
|
|
26
28
|
function getDatabasePath(explicitPath) {
|
|
27
29
|
if (explicitPath)
|
|
28
30
|
return resolve(explicitPath);
|
|
@@ -30,6 +32,51 @@ function getDatabasePath(explicitPath) {
|
|
|
30
32
|
return resolve(process.env.SHORTLINKS_DB);
|
|
31
33
|
return join(ensureDataDir(), `${SERVICE_NAME}.db`);
|
|
32
34
|
}
|
|
35
|
+
function readClickSaltFile(path) {
|
|
36
|
+
try {
|
|
37
|
+
const saved = readFileSync(path, "utf-8").trim();
|
|
38
|
+
return saved || null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function clickSaltError(path, error) {
|
|
44
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
45
|
+
return new Error(`Could not initialize click salt at ${path}. Set SHORTLINKS_CLICK_SALT or fix data directory permissions. ${detail}`);
|
|
46
|
+
}
|
|
47
|
+
function getClickSalt() {
|
|
48
|
+
const explicit = process.env.SHORTLINKS_CLICK_SALT?.trim();
|
|
49
|
+
if (explicit)
|
|
50
|
+
return explicit;
|
|
51
|
+
const path = getClickSaltPath();
|
|
52
|
+
const saved = readClickSaltFile(path);
|
|
53
|
+
if (saved)
|
|
54
|
+
return saved;
|
|
55
|
+
const generated = randomBytes(32).toString("hex");
|
|
56
|
+
const tempPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(tempPath, `${generated}
|
|
59
|
+
`, { flag: "wx", mode: 384 });
|
|
60
|
+
try {
|
|
61
|
+
linkSync(tempPath, path);
|
|
62
|
+
return generated;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const winner = readClickSaltFile(path);
|
|
65
|
+
if (winner)
|
|
66
|
+
return winner;
|
|
67
|
+
throw clickSaltError(path, error);
|
|
68
|
+
} finally {
|
|
69
|
+
try {
|
|
70
|
+
unlinkSync(tempPath);
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const winner = readClickSaltFile(path);
|
|
75
|
+
if (winner)
|
|
76
|
+
return winner;
|
|
77
|
+
throw clickSaltError(path, error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
33
80
|
function loadConfig() {
|
|
34
81
|
const path = getConfigPath();
|
|
35
82
|
if (!existsSync(path))
|
|
@@ -203,13 +250,13 @@ import { hostname } from "os";
|
|
|
203
250
|
import { join as join2 } from "path";
|
|
204
251
|
|
|
205
252
|
// src/slug.ts
|
|
206
|
-
import { randomBytes } from "crypto";
|
|
253
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
207
254
|
var SLUG_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
208
255
|
var DEFAULT_SLUG_LENGTH = 7;
|
|
209
256
|
function randomToken(length = DEFAULT_SLUG_LENGTH) {
|
|
210
257
|
if (length < 1 || length > 128)
|
|
211
258
|
throw new Error("Token length must be between 1 and 128.");
|
|
212
|
-
const bytes =
|
|
259
|
+
const bytes = randomBytes2(length);
|
|
213
260
|
let out = "";
|
|
214
261
|
for (let i = 0;i < length; i += 1) {
|
|
215
262
|
out += SLUG_ALPHABET[bytes[i] % SLUG_ALPHABET.length];
|
|
@@ -529,12 +576,177 @@ class ShortlinksStore {
|
|
|
529
576
|
throw new Error("Could not generate an unused slug after 32 attempts.");
|
|
530
577
|
}
|
|
531
578
|
hashIp(ip) {
|
|
532
|
-
|
|
533
|
-
return createHash("sha256").update(`${salt}:${ip}`).digest("hex");
|
|
579
|
+
return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
|
|
534
580
|
}
|
|
535
581
|
}
|
|
536
582
|
// src/pg-store.ts
|
|
537
583
|
import { createHash as createHash2 } from "crypto";
|
|
584
|
+
|
|
585
|
+
// src/runtime.ts
|
|
586
|
+
var SHORTLINKS_RUNTIME_ENV = {
|
|
587
|
+
store: "HASNA_SHORTLINKS_STORE",
|
|
588
|
+
databaseUrl: "HASNA_SHORTLINKS_DATABASE_URL",
|
|
589
|
+
databaseSsl: "HASNA_SHORTLINKS_DATABASE_SSL"
|
|
590
|
+
};
|
|
591
|
+
var SHORTLINKS_RUNTIME_FALLBACK_ENV = {
|
|
592
|
+
store: "SHORTLINKS_STORE",
|
|
593
|
+
databaseUrl: "SHORTLINKS_DATABASE_URL",
|
|
594
|
+
databaseSsl: "SHORTLINKS_DATABASE_SSL"
|
|
595
|
+
};
|
|
596
|
+
var CANONICAL_SHORTLINKS_POSTGRES_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
|
|
597
|
+
var CANONICAL_SHORTLINKS_POSTGRES_DATABASE = "shortlinks";
|
|
598
|
+
var CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH = "hasna/xyz/opensource/shortlinks/prod/postgres";
|
|
599
|
+
function getCanonicalShortlinksPostgresConfig() {
|
|
600
|
+
return {
|
|
601
|
+
cluster: CANONICAL_SHORTLINKS_POSTGRES_CLUSTER,
|
|
602
|
+
database: CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
|
|
603
|
+
runtimeSecretPath: CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
|
|
604
|
+
primaryEnv: SHORTLINKS_RUNTIME_ENV.databaseUrl,
|
|
605
|
+
fallbackEnv: SHORTLINKS_RUNTIME_FALLBACK_ENV.databaseUrl
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function parseShortlinksStoreMode(value) {
|
|
609
|
+
const normalized = clean(value)?.toLowerCase();
|
|
610
|
+
if (!normalized)
|
|
611
|
+
return "local";
|
|
612
|
+
if (normalized === "local" || normalized === "postgres")
|
|
613
|
+
return normalized;
|
|
614
|
+
if (normalized === "pg")
|
|
615
|
+
return "postgres";
|
|
616
|
+
throw new Error(`${SHORTLINKS_RUNTIME_ENV.store} must be local or postgres`);
|
|
617
|
+
}
|
|
618
|
+
function getShortlinksStoreMode(env = process.env) {
|
|
619
|
+
return parseShortlinksStoreMode(readRuntimeEnv(env, "store").value);
|
|
620
|
+
}
|
|
621
|
+
function getShortlinksDatabaseUrl(env = process.env) {
|
|
622
|
+
return readRuntimeEnv(env, "databaseUrl").value;
|
|
623
|
+
}
|
|
624
|
+
function getShortlinksDatabaseSsl(env = process.env) {
|
|
625
|
+
return parseBoolean(readRuntimeEnv(env, "databaseSsl").value, true);
|
|
626
|
+
}
|
|
627
|
+
function getShortlinksRuntimeEnvName(env, key) {
|
|
628
|
+
const primary = SHORTLINKS_RUNTIME_ENV[key];
|
|
629
|
+
if (clean(env[primary]))
|
|
630
|
+
return primary;
|
|
631
|
+
return SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
|
|
632
|
+
}
|
|
633
|
+
function loadShortlinksRuntimeConfig(env = process.env) {
|
|
634
|
+
const mode = getShortlinksStoreMode(env);
|
|
635
|
+
const databaseUrl = getShortlinksDatabaseUrl(env);
|
|
636
|
+
return {
|
|
637
|
+
service: "shortlinks",
|
|
638
|
+
mode,
|
|
639
|
+
...databaseUrl ? {
|
|
640
|
+
database: {
|
|
641
|
+
provider: "postgres",
|
|
642
|
+
url: databaseUrl,
|
|
643
|
+
ssl: getShortlinksDatabaseSsl(env)
|
|
644
|
+
}
|
|
645
|
+
} : {}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function assertShortlinksPostgresConfig(config) {
|
|
649
|
+
if (config.mode !== "postgres")
|
|
650
|
+
return;
|
|
651
|
+
if (!config.database?.url) {
|
|
652
|
+
throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseUrl} is required when ${SHORTLINKS_RUNTIME_ENV.store}=postgres`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function getShortlinksRuntimeStatus(env = process.env) {
|
|
656
|
+
const issues = [];
|
|
657
|
+
const warnings = [];
|
|
658
|
+
let config;
|
|
659
|
+
try {
|
|
660
|
+
config = loadShortlinksRuntimeConfig(env);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
663
|
+
config = { service: "shortlinks", mode: "local" };
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
assertShortlinksPostgresConfig(config);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
669
|
+
}
|
|
670
|
+
if (config.mode === "local" && config.database?.url) {
|
|
671
|
+
warnings.push(`${SHORTLINKS_RUNTIME_ENV.store}=local ignores configured Postgres database settings`);
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
ok: issues.length === 0,
|
|
675
|
+
service: "shortlinks",
|
|
676
|
+
mode: config.mode,
|
|
677
|
+
local_default: config.mode === "local",
|
|
678
|
+
postgres_enabled: config.mode === "postgres",
|
|
679
|
+
database: {
|
|
680
|
+
configured: Boolean(config.database?.url),
|
|
681
|
+
provider: config.database?.provider ?? null,
|
|
682
|
+
redacted_url: redactDatabaseUrl(config.database?.url),
|
|
683
|
+
ssl: config.database?.ssl ?? null
|
|
684
|
+
},
|
|
685
|
+
env: runtimeEnvStatus(env),
|
|
686
|
+
canonical: getCanonicalShortlinksPostgresConfig(),
|
|
687
|
+
issues,
|
|
688
|
+
warnings,
|
|
689
|
+
no_network: true
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function redactDatabaseUrl(value) {
|
|
693
|
+
if (!value)
|
|
694
|
+
return null;
|
|
695
|
+
try {
|
|
696
|
+
const url = new URL(value);
|
|
697
|
+
if (url.username)
|
|
698
|
+
url.username = "***";
|
|
699
|
+
if (url.password)
|
|
700
|
+
url.password = "***";
|
|
701
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
702
|
+
if (isSensitiveQueryKey(key))
|
|
703
|
+
url.searchParams.set(key, "***");
|
|
704
|
+
}
|
|
705
|
+
return url.toString();
|
|
706
|
+
} catch {
|
|
707
|
+
return "(redacted)";
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function runtimeEnvStatus(env) {
|
|
711
|
+
return Object.fromEntries(Object.entries(SHORTLINKS_RUNTIME_ENV).map(([key, name]) => {
|
|
712
|
+
const activeName = getShortlinksRuntimeEnvName(env, key);
|
|
713
|
+
return [
|
|
714
|
+
key,
|
|
715
|
+
{
|
|
716
|
+
name,
|
|
717
|
+
active_name: activeName,
|
|
718
|
+
configured: Boolean(clean(env[activeName]))
|
|
719
|
+
}
|
|
720
|
+
];
|
|
721
|
+
}));
|
|
722
|
+
}
|
|
723
|
+
function readRuntimeEnv(env, key) {
|
|
724
|
+
const primary = SHORTLINKS_RUNTIME_ENV[key];
|
|
725
|
+
const primaryValue = clean(env[primary]);
|
|
726
|
+
if (primaryValue)
|
|
727
|
+
return { name: primary, value: primaryValue };
|
|
728
|
+
const fallback = SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
|
|
729
|
+
return { name: fallback, value: clean(env[fallback]) };
|
|
730
|
+
}
|
|
731
|
+
function parseBoolean(value, fallback) {
|
|
732
|
+
const normalized = clean(value)?.toLowerCase();
|
|
733
|
+
if (!normalized)
|
|
734
|
+
return fallback;
|
|
735
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
736
|
+
return true;
|
|
737
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
738
|
+
return false;
|
|
739
|
+
throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseSsl} must be true or false`);
|
|
740
|
+
}
|
|
741
|
+
function isSensitiveQueryKey(key) {
|
|
742
|
+
return /(?:^|[_-])(pass(?:word)?|pwd|secret|token|credential|auth|api[_-]?key|access[_-]?key)(?:$|[_-])/i.test(key);
|
|
743
|
+
}
|
|
744
|
+
function clean(value) {
|
|
745
|
+
const trimmed = value?.trim();
|
|
746
|
+
return trimmed ? trimmed : undefined;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/pg-store.ts
|
|
538
750
|
function parseJsonObject2(value) {
|
|
539
751
|
if (!value)
|
|
540
752
|
return {};
|
|
@@ -547,6 +759,47 @@ function parseJsonObject2(value) {
|
|
|
547
759
|
return {};
|
|
548
760
|
}
|
|
549
761
|
}
|
|
762
|
+
async function loadPgPool() {
|
|
763
|
+
const importer = new Function("specifier", "return import(specifier)");
|
|
764
|
+
const module = await importer("pg");
|
|
765
|
+
return module.Pool;
|
|
766
|
+
}
|
|
767
|
+
function toPostgresSql(sql) {
|
|
768
|
+
let index = 0;
|
|
769
|
+
return sql.replace(/\?/g, () => `$${++index}`);
|
|
770
|
+
}
|
|
771
|
+
function createPgPoolConfig(connectionString, options = {}) {
|
|
772
|
+
const ssl = options.ssl ?? true;
|
|
773
|
+
return {
|
|
774
|
+
connectionString,
|
|
775
|
+
...ssl ? { ssl: { rejectUnauthorized: true } } : { ssl: false }
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
class PgPoolAdapter {
|
|
780
|
+
pool;
|
|
781
|
+
constructor(pool) {
|
|
782
|
+
this.pool = pool;
|
|
783
|
+
}
|
|
784
|
+
static async create(connectionString, options = {}) {
|
|
785
|
+
const Pool = await loadPgPool();
|
|
786
|
+
return new PgPoolAdapter(new Pool(createPgPoolConfig(connectionString, options)));
|
|
787
|
+
}
|
|
788
|
+
async get(sql, ...params) {
|
|
789
|
+
const result = await this.pool.query(toPostgresSql(sql), params);
|
|
790
|
+
return result.rows[0] ?? null;
|
|
791
|
+
}
|
|
792
|
+
async all(sql, ...params) {
|
|
793
|
+
const result = await this.pool.query(toPostgresSql(sql), params);
|
|
794
|
+
return result.rows;
|
|
795
|
+
}
|
|
796
|
+
async run(sql, ...params) {
|
|
797
|
+
return this.pool.query(toPostgresSql(sql), params);
|
|
798
|
+
}
|
|
799
|
+
async close() {
|
|
800
|
+
await this.pool.end();
|
|
801
|
+
}
|
|
802
|
+
}
|
|
550
803
|
function toIsoString(value) {
|
|
551
804
|
if (value instanceof Date)
|
|
552
805
|
return value.toISOString();
|
|
@@ -615,13 +868,15 @@ class PgShortlinksStore {
|
|
|
615
868
|
constructor(pg) {
|
|
616
869
|
this.pg = pg;
|
|
617
870
|
}
|
|
618
|
-
static async fromConnectionString(connectionString) {
|
|
619
|
-
|
|
620
|
-
return new PgShortlinksStore(new PgAdapterAsync(connectionString));
|
|
871
|
+
static async fromConnectionString(connectionString, options = {}) {
|
|
872
|
+
return new PgShortlinksStore(await PgPoolAdapter.create(connectionString, options));
|
|
621
873
|
}
|
|
622
|
-
static async
|
|
623
|
-
const
|
|
624
|
-
|
|
874
|
+
static async fromEnv(env = process.env) {
|
|
875
|
+
const connectionString = getShortlinksDatabaseUrl(env);
|
|
876
|
+
if (!connectionString) {
|
|
877
|
+
throw new Error("HASNA_SHORTLINKS_DATABASE_URL is required when shortlinks uses the postgres store");
|
|
878
|
+
}
|
|
879
|
+
return PgShortlinksStore.fromConnectionString(connectionString, { ssl: getShortlinksDatabaseSsl(env) });
|
|
625
880
|
}
|
|
626
881
|
async close() {
|
|
627
882
|
await this.pg.close?.();
|
|
@@ -837,8 +1092,7 @@ class PgShortlinksStore {
|
|
|
837
1092
|
};
|
|
838
1093
|
}
|
|
839
1094
|
hashIp(ip) {
|
|
840
|
-
|
|
841
|
-
return createHash2("sha256").update(`${salt}:${ip}`).digest("hex");
|
|
1095
|
+
return createHash2("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
|
|
842
1096
|
}
|
|
843
1097
|
async generateAvailableSlug(domainId, length) {
|
|
844
1098
|
for (let attempt = 0;attempt < 32; attempt += 1) {
|
|
@@ -852,6 +1106,52 @@ class PgShortlinksStore {
|
|
|
852
1106
|
throw new Error("Could not generate an unused slug after 32 attempts.");
|
|
853
1107
|
}
|
|
854
1108
|
}
|
|
1109
|
+
async function applyPostgresMigrations(connectionString, migrations, options = {}) {
|
|
1110
|
+
const Pool = await loadPgPool();
|
|
1111
|
+
const pool = new Pool(createPgPoolConfig(connectionString, options));
|
|
1112
|
+
const client = await pool.connect();
|
|
1113
|
+
const run = (sql, ...params) => client.query(toPostgresSql(sql), params);
|
|
1114
|
+
const get = async (sql, ...params) => {
|
|
1115
|
+
const result = await run(sql, ...params);
|
|
1116
|
+
return result.rows[0] ?? null;
|
|
1117
|
+
};
|
|
1118
|
+
const applied = [];
|
|
1119
|
+
const skipped = [];
|
|
1120
|
+
try {
|
|
1121
|
+
await run("BEGIN");
|
|
1122
|
+
await run(`
|
|
1123
|
+
SELECT pg_advisory_xact_lock(hashtext(?))
|
|
1124
|
+
`, "shortlinks:migrations");
|
|
1125
|
+
await run(`
|
|
1126
|
+
CREATE TABLE IF NOT EXISTS _shortlinks_migrations (
|
|
1127
|
+
id INTEGER PRIMARY KEY,
|
|
1128
|
+
service TEXT NOT NULL DEFAULT 'shortlinks',
|
|
1129
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
1130
|
+
)
|
|
1131
|
+
`);
|
|
1132
|
+
for (let i = 0;i < migrations.length; i += 1) {
|
|
1133
|
+
const id = i + 1;
|
|
1134
|
+
const existing = await get("SELECT id FROM _shortlinks_migrations WHERE id = ? LIMIT 1", id);
|
|
1135
|
+
if (existing) {
|
|
1136
|
+
skipped.push(id);
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
await run(migrations[i]);
|
|
1140
|
+
await run("INSERT INTO _shortlinks_migrations (id, service, applied_at) VALUES (?, ?, now())", id, "shortlinks");
|
|
1141
|
+
applied.push(id);
|
|
1142
|
+
}
|
|
1143
|
+
await run("COMMIT");
|
|
1144
|
+
return { service: "shortlinks", applied, skipped };
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
try {
|
|
1147
|
+
await run("ROLLBACK");
|
|
1148
|
+
} catch {}
|
|
1149
|
+
throw error;
|
|
1150
|
+
} finally {
|
|
1151
|
+
client.release();
|
|
1152
|
+
await pool.end();
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
855
1155
|
// src/server.ts
|
|
856
1156
|
var REDIRECT_ALLOW_HEADER = "GET, HEAD";
|
|
857
1157
|
function json(data, status = 200, headers) {
|
|
@@ -1202,23 +1502,39 @@ export {
|
|
|
1202
1502
|
serveShortlinks,
|
|
1203
1503
|
saveConfig,
|
|
1204
1504
|
registerMachinesDns,
|
|
1505
|
+
redactDatabaseUrl,
|
|
1205
1506
|
randomToken,
|
|
1507
|
+
parseShortlinksStoreMode,
|
|
1206
1508
|
now,
|
|
1207
1509
|
normalizeSlug,
|
|
1208
1510
|
normalizeHostname,
|
|
1209
1511
|
makeId,
|
|
1512
|
+
loadShortlinksRuntimeConfig,
|
|
1210
1513
|
loadConfig,
|
|
1514
|
+
getShortlinksStoreMode,
|
|
1515
|
+
getShortlinksRuntimeStatus,
|
|
1516
|
+
getShortlinksRuntimeEnvName,
|
|
1517
|
+
getShortlinksDatabaseUrl,
|
|
1518
|
+
getShortlinksDatabaseSsl,
|
|
1211
1519
|
getDatabasePath,
|
|
1212
1520
|
getDataDir,
|
|
1213
1521
|
getConfigPath,
|
|
1522
|
+
getCanonicalShortlinksPostgresConfig,
|
|
1214
1523
|
generateWorkerScript,
|
|
1215
1524
|
formatShortUrl,
|
|
1216
1525
|
createShortlinksHandler,
|
|
1217
1526
|
createLocalSetupPlan,
|
|
1218
1527
|
createCloudflarePlan,
|
|
1528
|
+
assertShortlinksPostgresConfig,
|
|
1529
|
+
applyPostgresMigrations,
|
|
1219
1530
|
ShortlinksStore,
|
|
1220
1531
|
ShortlinksDatabase,
|
|
1221
1532
|
SQLITE_MIGRATIONS,
|
|
1533
|
+
SHORTLINKS_RUNTIME_FALLBACK_ENV,
|
|
1534
|
+
SHORTLINKS_RUNTIME_ENV,
|
|
1222
1535
|
PgShortlinksStore,
|
|
1223
|
-
PG_MIGRATIONS
|
|
1536
|
+
PG_MIGRATIONS,
|
|
1537
|
+
CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
|
|
1538
|
+
CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
|
|
1539
|
+
CANONICAL_SHORTLINKS_POSTGRES_CLUSTER
|
|
1224
1540
|
};
|
package/dist/pg-store.d.ts
CHANGED
|
@@ -5,11 +5,14 @@ type PgAdapterLike = {
|
|
|
5
5
|
run(sql: string, ...params: unknown[]): Promise<unknown>;
|
|
6
6
|
close?: () => Promise<void>;
|
|
7
7
|
};
|
|
8
|
+
export interface PgConnectionOptions {
|
|
9
|
+
ssl?: boolean;
|
|
10
|
+
}
|
|
8
11
|
export declare class PgShortlinksStore {
|
|
9
12
|
private readonly pg;
|
|
10
13
|
constructor(pg: PgAdapterLike);
|
|
11
|
-
static fromConnectionString(connectionString: string): Promise<PgShortlinksStore>;
|
|
12
|
-
static
|
|
14
|
+
static fromConnectionString(connectionString: string, options?: PgConnectionOptions): Promise<PgShortlinksStore>;
|
|
15
|
+
static fromEnv(env?: NodeJS.ProcessEnv): Promise<PgShortlinksStore>;
|
|
13
16
|
close(): Promise<void>;
|
|
14
17
|
addDomain(input: AddDomainInput): Promise<Domain>;
|
|
15
18
|
listDomains(): Promise<Domain[]>;
|
|
@@ -35,4 +38,9 @@ export declare class PgShortlinksStore {
|
|
|
35
38
|
private hashIp;
|
|
36
39
|
private generateAvailableSlug;
|
|
37
40
|
}
|
|
41
|
+
export declare function applyPostgresMigrations(connectionString: string, migrations: string[], options?: PgConnectionOptions): Promise<{
|
|
42
|
+
service: "shortlinks";
|
|
43
|
+
applied: number[];
|
|
44
|
+
skipped: number[];
|
|
45
|
+
}>;
|
|
38
46
|
export {};
|