@grapity/grapity 0.2.0 → 0.3.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/dist/cli/index.js CHANGED
@@ -21,7 +21,8 @@ import yaml from "js-yaml";
21
21
  var DEFAULT_CONFIG = {
22
22
  mode: "local",
23
23
  local: {
24
- port: 3750
24
+ port: 3750,
25
+ database: "sqlite"
25
26
  }
26
27
  };
27
28
  function getConfig() {
@@ -40,12 +41,18 @@ function getConfig() {
40
41
  return DEFAULT_CONFIG;
41
42
  }
42
43
  const config = parsed;
44
+ let database = config.local?.database ?? DEFAULT_CONFIG.local.database;
45
+ if (!config.local?.database && config.local?.sqlitePath) {
46
+ database = "sqlite";
47
+ }
43
48
  return {
44
49
  mode: config.mode ?? DEFAULT_CONFIG.mode,
45
50
  remote: config.remote,
46
51
  local: {
47
52
  port: config.local?.port ?? DEFAULT_CONFIG.local.port,
48
- sqlitePath: config.local?.sqlitePath
53
+ database,
54
+ sqlitePath: config.local?.sqlitePath,
55
+ postgresUrl: config.local?.postgresUrl
49
56
  }
50
57
  };
51
58
  }
@@ -56,6 +63,9 @@ function getRegistryUrl() {
56
63
  }
57
64
  return `http://localhost:${config.local?.port ?? 3750}`;
58
65
  }
66
+ function isPostgresqlUrl(value) {
67
+ return value.startsWith("postgresql://") || value.startsWith("postgres://");
68
+ }
59
69
 
60
70
  // src/cli/client.ts
61
71
  var BreakingChangeError = class extends Error {
@@ -69,16 +79,11 @@ var BreakingChangeError = class extends Error {
69
79
  async function request(method, path12, body) {
70
80
  const baseUrl = getRegistryUrl();
71
81
  const url = `${baseUrl}${path12}`;
72
- const config = getConfig();
73
- const headers = {
74
- "Content-Type": "application/json"
75
- };
76
- if (config.mode === "remote" && config.remote?.apiKey) {
77
- headers["X-API-Key"] = config.remote.apiKey;
78
- }
79
82
  const response = await fetch(url, {
80
83
  method,
81
- headers,
84
+ headers: {
85
+ "Content-Type": "application/json"
86
+ },
82
87
  body: body ? JSON.stringify(body) : void 0
83
88
  });
84
89
  if (!response.ok) {
@@ -88,18 +93,13 @@ async function request(method, path12, body) {
88
93
  }
89
94
  throw new Error(error.message ?? `Request failed: ${response.status}`);
90
95
  }
91
- const text2 = await response.text();
92
- return text2 ? JSON.parse(text2) : void 0;
96
+ const text3 = await response.text();
97
+ return text3 ? JSON.parse(text3) : void 0;
93
98
  }
94
99
  async function requestText(method, path12) {
95
100
  const baseUrl = getRegistryUrl();
96
101
  const url = `${baseUrl}${path12}`;
97
- const config = getConfig();
98
- const headers = {};
99
- if (config.mode === "remote" && config.remote?.apiKey) {
100
- headers["X-API-Key"] = config.remote.apiKey;
101
- }
102
- const response = await fetch(url, { method, headers });
102
+ const response = await fetch(url, { method });
103
103
  if (!response.ok) {
104
104
  const error = await response.json();
105
105
  throw new Error(error.message ?? `Request failed: ${response.status}`);
@@ -150,9 +150,9 @@ var client = {
150
150
  fetchSpec: async (name, options) => {
151
151
  const format = options.format ?? "yaml";
152
152
  const path12 = options.semver ? `/v1/specs/${name}/versions/${options.semver}/spec.${format}` : `/v1/specs/${name}/spec.${format}`;
153
- const { text: text2, headers } = await requestText("GET", path12);
153
+ const { text: text3, headers } = await requestText("GET", path12);
154
154
  return {
155
- content: text2,
155
+ content: text3,
156
156
  resolvedVersion: headers.get("Grapity-Resolved-Version") ?? void 0
157
157
  };
158
158
  },
@@ -531,27 +531,43 @@ function formatInitSuccess(params) {
531
531
  ];
532
532
  if (params.mode === "local") {
533
533
  if (params.port) lines.push(` ${c.label("Port")} ${c.cyan(String(params.port))}`);
534
- if (params.dbPath) lines.push(` ${c.label("Database")} ${c.dim(params.dbPath)}`);
534
+ if (params.database) lines.push(` ${c.label("Database")} ${c.primary(params.database)}`);
535
+ if (params.dbPath) lines.push(` ${c.label("Path")} ${c.dim(params.dbPath)}`);
536
+ if (params.postgresUrl) lines.push(` ${c.label("PostgreSQL")} ${c.dim(params.postgresUrl)}`);
535
537
  lines.push("");
536
538
  lines.push(` ${c.dim("\u203A")} Start the server with: ${c.primary("grapity serve")}`);
537
539
  } else {
538
540
  if (params.url) lines.push(` ${c.label("URL")} ${c.cyan(params.url)}`);
539
- if (params.hasApiKey) lines.push(` ${c.label("API key")} ${c.dim("configured")}`);
540
541
  lines.push("");
541
542
  lines.push(` ${c.dim("\u203A")} Push a spec with: ${c.primary("grapity registry push ./openapi.yaml --name my-api")}`);
542
543
  }
543
544
  return lines.join("\n");
544
545
  }
545
546
  function formatServeConfig(params) {
546
- const modeLabel = `local ${c.label("\xB7")} ${params.mode}`;
547
547
  const lines = [
548
- ` ${c.label("Mode")} ${c.primary(modeLabel)}`,
549
- ` ${c.label("Port")} ${c.cyan(String(params.port))}`
548
+ ` ${c.label("Mode")} ${c.primary("local")}`,
549
+ ` ${c.label("Port")} ${c.cyan(String(params.port))}`,
550
+ ` ${c.label("Database")} ${c.primary(params.database)}`
550
551
  ];
551
- if (params.dbPath) lines.push(` ${c.label("Database")} ${c.dim(params.dbPath)}`);
552
- lines.push(` ${c.label("Auth")} ${c.primary(params.auth)}`);
552
+ if (params.database === "sqlite" && params.dbPath) {
553
+ lines.push(` ${c.label("Path")} ${c.dim(params.dbPath)}`);
554
+ }
555
+ if (params.database === "postgresql" && params.postgresUrl) {
556
+ lines.push(` ${c.label("PostgreSQL")} ${c.dim(maskPostgresUrl(params.postgresUrl))}`);
557
+ }
553
558
  return lines.join("\n");
554
559
  }
560
+ function maskPostgresUrl(url) {
561
+ try {
562
+ const parsed = new URL(url);
563
+ if (parsed.password) {
564
+ parsed.password = "***";
565
+ }
566
+ return parsed.toString();
567
+ } catch {
568
+ return url;
569
+ }
570
+ }
555
571
  function formatHubConfig(params) {
556
572
  const lines = [
557
573
  ` ${c.label("Port")} ${c.cyan(String(params.port))}`,
@@ -744,18 +760,18 @@ var validateCommand = new Command2("validate").description("Validate a spec agai
744
760
  import { Command as Command3 } from "commander";
745
761
  var listCommand = new Command3("list").description("List all specs in the registry").option("--type <type>", "Filter by spec type").option("--owner <owner>", "Filter by owner").option("--tags <tags>", "Filter by tags (comma-separated)").action(async (options) => {
746
762
  try {
747
- const specs2 = await client.listSpecs({
763
+ const specs3 = await client.listSpecs({
748
764
  type: options.type,
749
765
  owner: options.owner,
750
766
  tags: options.tags?.split(",")
751
767
  });
752
- if (specs2.length === 0) {
768
+ if (specs3.length === 0) {
753
769
  console.log(formatEmptyState("No specs in the registry.", [
754
770
  "Push one with: grapity registry push ./openapi.yaml --name my-api"
755
771
  ]));
756
772
  return;
757
773
  }
758
- for (const spec of specs2) {
774
+ for (const spec of specs3) {
759
775
  console.log(formatSpec(spec));
760
776
  }
761
777
  } catch (err) {
@@ -1145,10 +1161,10 @@ function convertPathToKong(path12) {
1145
1161
  }
1146
1162
  return converted;
1147
1163
  }
1148
- function routeName(serviceName, index2, path12, methods) {
1164
+ function routeName(serviceName, index3, path12, methods) {
1149
1165
  const cleanPath = path12.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1150
1166
  const methodList = methods.join("-").toLowerCase();
1151
- return `${serviceName}-${cleanPath}-${methodList}-${index2}`;
1167
+ return `${serviceName}-${cleanPath}-${methodList}-${index3}`;
1152
1168
  }
1153
1169
  function generateDeckYaml(config, envName) {
1154
1170
  const env = config.environments[envName];
@@ -1380,7 +1396,7 @@ import fs8 from "fs";
1380
1396
  import os3 from "os";
1381
1397
  import path8 from "path";
1382
1398
  import yaml4 from "js-yaml";
1383
- var initCommand = new Command18("init").description("Configure grapity registry (local or remote mode)").option("--local", "Use local mode (SQLite)").option("--remote", "Use remote mode (connect to a grapity server)").option("--url <url>", "Registry URL (for remote mode)").option("--api-key <key>", "API key (for remote mode)").option("--port <port>", "Port for local server (default: 3750)").option("--db <path>", "Path to SQLite database file (for local mode)").action(async (options) => {
1399
+ var initCommand = new Command18("init").description("Configure grapity registry (local or remote mode)").option("--local", "Use local mode (SQLite or PostgreSQL)").option("--remote", "Use remote mode (connect to a grapity server)").option("--url <url>", "Registry URL (for remote mode)").option("--port <port>", "Port for local server (default: 3750)").option("--db <path-or-url>", "SQLite path or postgresql:// URL (for local mode)").action(async (options) => {
1384
1400
  const configDir = path8.join(os3.homedir(), ".grapity");
1385
1401
  const configPath = path8.join(configDir, "config.yaml");
1386
1402
  let mode;
@@ -1398,7 +1414,7 @@ var initCommand = new Command18("init").description("Configure grapity registry
1398
1414
  "missing flag",
1399
1415
  "Select registry mode: use --local or --remote.",
1400
1416
  [
1401
- "--local Run a registry server on this machine (SQLite)",
1417
+ "--local Run a registry server on this machine",
1402
1418
  "--remote Connect to an existing grapity server"
1403
1419
  ]
1404
1420
  )
@@ -1407,12 +1423,16 @@ var initCommand = new Command18("init").description("Configure grapity registry
1407
1423
  }
1408
1424
  const config = { mode };
1409
1425
  if (mode === "local") {
1426
+ const dbValue = options.db ?? process.env.GRAPITY_DATABASE_URL;
1427
+ const database = dbValue && isPostgresqlUrl(dbValue) ? "postgresql" : "sqlite";
1410
1428
  config.local = {
1411
1429
  port: options.port ? parseInt(options.port, 10) : 3750,
1412
- sqlitePath: options.db
1430
+ database
1413
1431
  };
1414
- if (!config.local.sqlitePath) {
1415
- config.local.sqlitePath = path8.join(os3.homedir(), ".grapity", "registry.db");
1432
+ if (database === "postgresql") {
1433
+ config.local.postgresUrl = dbValue;
1434
+ } else {
1435
+ config.local.sqlitePath = dbValue ?? path8.join(os3.homedir(), ".grapity", "registry.db");
1416
1436
  }
1417
1437
  } else {
1418
1438
  if (!options.url) {
@@ -1426,8 +1446,7 @@ var initCommand = new Command18("init").description("Configure grapity registry
1426
1446
  process.exit(1);
1427
1447
  }
1428
1448
  config.remote = {
1429
- url: options.url.replace(/\/$/, ""),
1430
- apiKey: options.apiKey
1449
+ url: options.url.replace(/\/$/, "")
1431
1450
  };
1432
1451
  }
1433
1452
  if (!fs8.existsSync(configDir)) {
@@ -1440,9 +1459,10 @@ var initCommand = new Command18("init").description("Configure grapity registry
1440
1459
  configPath,
1441
1460
  mode,
1442
1461
  port: config.local?.port,
1462
+ database: config.local?.database,
1443
1463
  dbPath: config.local?.sqlitePath,
1444
- url: config.remote?.url,
1445
- hasApiKey: !!config.remote?.apiKey
1464
+ postgresUrl: config.local?.postgresUrl,
1465
+ url: config.remote?.url
1446
1466
  })
1447
1467
  );
1448
1468
  });
@@ -3090,9 +3110,9 @@ var RegistryService = class {
3090
3110
  return { spec, version: version2, compatReport, isNewSpec };
3091
3111
  }
3092
3112
  async listSpecs(filters) {
3093
- const specs2 = await this.store.listSpecs(filters);
3113
+ const specs3 = await this.store.listSpecs(filters);
3094
3114
  return Promise.all(
3095
- specs2.map(async (spec) => {
3115
+ specs3.map(async (spec) => {
3096
3116
  const latestVersion = await this.store.getLatestVersion(spec.name);
3097
3117
  return { ...spec, latestVersion: latestVersion ?? void 0 };
3098
3118
  })
@@ -3350,8 +3370,8 @@ var listRoute = new Hono3().get("/", async (c2) => {
3350
3370
  const type = c2.req.query("type");
3351
3371
  const owner = c2.req.query("owner");
3352
3372
  const tags = c2.req.query("tags")?.split(",");
3353
- const specs2 = await service.listSpecs({ type, owner, tags });
3354
- return c2.json({ data: specs2 });
3373
+ const specs3 = await service.listSpecs({ type, owner, tags });
3374
+ return c2.json({ data: specs3 });
3355
3375
  });
3356
3376
 
3357
3377
  // src/registry/routes/get-spec.ts
@@ -3985,8 +4005,7 @@ function switchTab(tab) {
3985
4005
  }
3986
4006
  var welcomeRoute = new Hono12().get("/", (c2) => {
3987
4007
  const config = c2.get("config");
3988
- const mode = config.database === "sqlite" ? "local" : "remote";
3989
- return c2.html(buildPage(config.port, mode));
4008
+ return c2.html(buildPage(config.port, "local"));
3990
4009
  });
3991
4010
 
3992
4011
  // src/registry/routes/push-gateway-config.ts
@@ -4327,7 +4346,6 @@ var ingestGatewayLogRoute = new Hono18().post("/ingest/:provider/:environment",
4327
4346
  await service.ingestLog(provider, environment, payload);
4328
4347
  return c2.json({ status: "ok" }, 201);
4329
4348
  } catch (err) {
4330
- console.error("Gateway log ingest error:", err);
4331
4349
  return c2.json({
4332
4350
  error: "bad_request",
4333
4351
  message: err instanceof Error ? err.message : "Invalid log payload",
@@ -4440,9 +4458,7 @@ function createApp(config, store) {
4440
4458
  // src/registry/config.ts
4441
4459
  var defaultConfig = {
4442
4460
  port: 3750,
4443
- database: "sqlite",
4444
- sqlitePath: void 0,
4445
- gracePeriodDays: 30
4461
+ database: "sqlite"
4446
4462
  };
4447
4463
 
4448
4464
  // src/registry/storage/sqlite.ts
@@ -4948,27 +4964,529 @@ var SQLiteSpecStore = class {
4948
4964
  }
4949
4965
  };
4950
4966
 
4967
+ // src/registry/storage/postgresql.ts
4968
+ import { Pool } from "pg";
4969
+ import { drizzle as drizzle2 } from "drizzle-orm/node-postgres";
4970
+ import { migrate as migrate2 } from "drizzle-orm/node-postgres/migrator";
4971
+ import { eq as eq2, and as and2, desc as desc2, sql as sql2 } from "drizzle-orm";
4972
+
4973
+ // src/registry/storage/schema-pg.ts
4974
+ import { pgTable, text as text2, timestamp, boolean, jsonb, index as index2, integer as integer2 } from "drizzle-orm/pg-core";
4975
+ var specs2 = pgTable("specs", {
4976
+ id: text2("id").primaryKey(),
4977
+ name: text2("name").notNull().unique(),
4978
+ type: text2("type", { enum: ["openapi", "asyncapi"] }).notNull(),
4979
+ description: text2("description"),
4980
+ owner: text2("owner"),
4981
+ sourceRepo: text2("source_repo"),
4982
+ tags: jsonb("tags").$type().default([]),
4983
+ createdAt: timestamp("created_at").notNull(),
4984
+ updatedAt: timestamp("updated_at").notNull()
4985
+ });
4986
+ var specVersions2 = pgTable("spec_versions", {
4987
+ id: text2("id").primaryKey(),
4988
+ specId: text2("spec_id").notNull().references(() => specs2.id),
4989
+ semver: text2("semver").notNull(),
4990
+ content: text2("content").notNull(),
4991
+ checksum: text2("checksum").notNull(),
4992
+ gitRef: text2("git_ref"),
4993
+ pushedBy: text2("pushed_by"),
4994
+ compatibility: jsonb("compatibility").$type(),
4995
+ previousVersion: text2("previous_version"),
4996
+ forceReason: text2("force_reason"),
4997
+ isPrerelease: boolean("is_prerelease").notNull().default(false),
4998
+ createdAt: timestamp("created_at").notNull()
4999
+ }, (table) => [
5000
+ index2("idx_spec_versions_spec_id").on(table.specId),
5001
+ index2("idx_spec_versions_semver").on(table.specId, table.semver)
5002
+ ]);
5003
+ var auditLog2 = pgTable("audit_log", {
5004
+ id: text2("id").primaryKey(),
5005
+ action: text2("action", { enum: ["spec.push", "spec.push.force", "spec.delete"] }).notNull(),
5006
+ actor: text2("actor").notNull(),
5007
+ specName: text2("spec_name").notNull(),
5008
+ version: text2("version"),
5009
+ details: jsonb("details").$type(),
5010
+ createdAt: timestamp("created_at").notNull()
5011
+ }, (table) => [
5012
+ index2("idx_audit_log_spec_name").on(table.specName),
5013
+ index2("idx_audit_log_created_at").on(table.createdAt)
5014
+ ]);
5015
+ var gatewayConfigs2 = pgTable("gateway_configs", {
5016
+ id: text2("id").primaryKey(),
5017
+ name: text2("name").notNull().unique(),
5018
+ provider: text2("provider", { enum: ["kong"] }).notNull(),
5019
+ specName: text2("spec_name").notNull(),
5020
+ specSemver: text2("spec_semver").notNull(),
5021
+ createdAt: timestamp("created_at").notNull(),
5022
+ updatedAt: timestamp("updated_at").notNull()
5023
+ });
5024
+ var gatewayConfigVersions2 = pgTable("gateway_config_versions", {
5025
+ id: text2("id").primaryKey(),
5026
+ gatewayConfigId: text2("gateway_config_id").notNull().references(() => gatewayConfigs2.id),
5027
+ routes: jsonb("routes").$type().notNull(),
5028
+ environments: jsonb("environments").$type().notNull(),
5029
+ callerIdentification: jsonb("caller_identification").$type(),
5030
+ content: text2("content").notNull(),
5031
+ checksum: text2("checksum").notNull(),
5032
+ pushedBy: text2("pushed_by"),
5033
+ createdAt: timestamp("created_at").notNull()
5034
+ }, (table) => [
5035
+ index2("idx_gateway_config_versions_config_id").on(table.gatewayConfigId)
5036
+ ]);
5037
+ var httpLogs2 = pgTable("http_logs", {
5038
+ id: text2("id").primaryKey(),
5039
+ provider: text2("provider").notNull(),
5040
+ gatewayConfigName: text2("gateway_config_name").notNull(),
5041
+ environment: text2("environment").notNull(),
5042
+ method: text2("method").notNull(),
5043
+ path: text2("path").notNull(),
5044
+ routePath: text2("route_path"),
5045
+ status: integer2("status").notNull(),
5046
+ callerId: text2("caller_id"),
5047
+ callerSource: text2("caller_source"),
5048
+ callerConfidence: text2("caller_confidence").notNull(),
5049
+ occurredAt: timestamp("occurred_at").notNull(),
5050
+ createdAt: timestamp("created_at").notNull()
5051
+ }, (table) => [
5052
+ index2("idx_http_logs_config_env").on(table.gatewayConfigName, table.environment),
5053
+ index2("idx_http_logs_occurred_at").on(table.occurredAt),
5054
+ index2("idx_http_logs_caller").on(table.gatewayConfigName, table.environment, table.callerId)
5055
+ ]);
5056
+ var provisions2 = pgTable("provisions", {
5057
+ id: text2("id").primaryKey(),
5058
+ gatewayConfigName: text2("gateway_config_name").notNull(),
5059
+ gatewayConfigVersion: text2("gateway_config_version").notNull(),
5060
+ environment: text2("environment").notNull(),
5061
+ provider: text2("provider", { enum: ["kong"] }).notNull(),
5062
+ synced: boolean("synced").notNull().default(false),
5063
+ actor: text2("actor").notNull(),
5064
+ details: jsonb("details").$type(),
5065
+ createdAt: timestamp("created_at").notNull()
5066
+ }, (table) => [
5067
+ index2("idx_provisions_config_name").on(table.gatewayConfigName),
5068
+ index2("idx_provisions_created_at").on(table.createdAt)
5069
+ ]);
5070
+
5071
+ // src/registry/storage/postgresql.ts
5072
+ import { v4 as uuid6 } from "uuid";
5073
+ var MIGRATIONS_FOLDER2 = PG_MIGRATIONS_FOLDER;
5074
+ var PostgreSQLSpecStore = class {
5075
+ db;
5076
+ pool;
5077
+ constructor(postgresUrl) {
5078
+ this.pool = new Pool({ connectionString: postgresUrl });
5079
+ this.db = drizzle2(this.pool);
5080
+ }
5081
+ async migrate() {
5082
+ await migrate2(this.db, {
5083
+ migrationsFolder: MIGRATIONS_FOLDER2
5084
+ });
5085
+ }
5086
+ async end() {
5087
+ await this.pool.end();
5088
+ }
5089
+ async getSpec(name) {
5090
+ const rows = await this.db.select().from(specs2).where(eq2(specs2.name, name)).limit(1);
5091
+ if (rows.length === 0) return null;
5092
+ return this.mapSpecRow(rows[0]);
5093
+ }
5094
+ async getSpecVersion(name, semver) {
5095
+ const spec = await this.getSpec(name);
5096
+ if (!spec) return null;
5097
+ const rows = await this.db.select().from(specVersions2).where(and2(eq2(specVersions2.specId, spec.id), eq2(specVersions2.semver, semver))).limit(1);
5098
+ if (rows.length === 0) return null;
5099
+ return this.mapVersionRow(rows[0]);
5100
+ }
5101
+ async getLatestVersion(name) {
5102
+ const spec = await this.getSpec(name);
5103
+ if (!spec) return null;
5104
+ const rows = await this.db.select().from(specVersions2).where(eq2(specVersions2.specId, spec.id)).orderBy(desc2(specVersions2.createdAt), desc2(specVersions2.id)).limit(1);
5105
+ if (rows.length === 0) return null;
5106
+ return this.mapVersionRow(rows[0]);
5107
+ }
5108
+ async listSpecs(filters) {
5109
+ const conditions = [];
5110
+ if (filters?.type) conditions.push(eq2(specs2.type, filters.type));
5111
+ if (filters?.owner) conditions.push(eq2(specs2.owner, filters.owner));
5112
+ let rows = conditions.length > 0 ? await this.db.select().from(specs2).where(and2(...conditions)) : await this.db.select().from(specs2);
5113
+ if (filters?.tags && filters.tags.length > 0) {
5114
+ rows = rows.filter((row) => {
5115
+ const rowTags = row.tags ?? [];
5116
+ return filters.tags.every((tag) => rowTags.includes(tag));
5117
+ });
5118
+ }
5119
+ return rows.map((r) => this.mapSpecRow(r));
5120
+ }
5121
+ async listVersions(name, options) {
5122
+ const spec = await this.getSpec(name);
5123
+ if (!spec) return { versions: [], total: 0 };
5124
+ const limit = options?.limit ?? 10;
5125
+ const offset = options?.offset ?? 0;
5126
+ const [countRow] = await this.db.select({ count: sql2`count(*)` }).from(specVersions2).where(eq2(specVersions2.specId, spec.id));
5127
+ const total = Number(countRow?.count ?? 0);
5128
+ const rows = await this.db.select().from(specVersions2).where(eq2(specVersions2.specId, spec.id)).orderBy(desc2(specVersions2.createdAt), desc2(specVersions2.id)).limit(limit).offset(offset);
5129
+ return { versions: rows.map((r) => this.mapVersionRow(r)), total };
5130
+ }
5131
+ async pushSpecVersion(spec, version2) {
5132
+ const existingSpec = await this.getSpec(spec.name);
5133
+ if (!existingSpec) {
5134
+ await this.db.insert(specs2).values({
5135
+ id: spec.id,
5136
+ name: spec.name,
5137
+ type: spec.type,
5138
+ description: spec.description ?? null,
5139
+ owner: spec.owner ?? null,
5140
+ sourceRepo: spec.sourceRepo ?? null,
5141
+ tags: spec.tags ?? [],
5142
+ createdAt: spec.createdAt,
5143
+ updatedAt: spec.updatedAt
5144
+ });
5145
+ } else {
5146
+ await this.db.update(specs2).set({ updatedAt: /* @__PURE__ */ new Date() }).where(eq2(specs2.id, existingSpec.id));
5147
+ }
5148
+ const specId = existingSpec?.id ?? spec.id;
5149
+ await this.db.insert(specVersions2).values({
5150
+ id: version2.id,
5151
+ specId,
5152
+ semver: version2.semver,
5153
+ content: version2.content,
5154
+ checksum: version2.checksum,
5155
+ gitRef: version2.gitRef ?? null,
5156
+ pushedBy: version2.pushedBy ?? null,
5157
+ compatibility: version2.compatibility ?? null,
5158
+ previousVersion: version2.previousVersion ?? null,
5159
+ forceReason: version2.forceReason ?? null,
5160
+ isPrerelease: version2.isPrerelease,
5161
+ createdAt: version2.createdAt
5162
+ });
5163
+ return version2;
5164
+ }
5165
+ async deleteSpec(name) {
5166
+ const existingSpec = await this.getSpec(name);
5167
+ if (!existingSpec) return false;
5168
+ await this.db.delete(specVersions2).where(eq2(specVersions2.specId, existingSpec.id));
5169
+ await this.db.delete(specs2).where(eq2(specs2.id, existingSpec.id));
5170
+ return true;
5171
+ }
5172
+ async getCompatReport(name, semver) {
5173
+ const version2 = await this.getSpecVersion(name, semver);
5174
+ return version2?.compatibility ?? null;
5175
+ }
5176
+ async logAudit(action, actor, specName, version2, details) {
5177
+ await this.db.insert(auditLog2).values({
5178
+ id: uuid6(),
5179
+ action,
5180
+ actor,
5181
+ specName,
5182
+ version: version2 ?? null,
5183
+ details: details ?? null,
5184
+ createdAt: /* @__PURE__ */ new Date()
5185
+ });
5186
+ }
5187
+ mapSpecRow(row) {
5188
+ return {
5189
+ id: row.id,
5190
+ name: row.name,
5191
+ type: row.type,
5192
+ description: row.description ?? void 0,
5193
+ owner: row.owner ?? void 0,
5194
+ sourceRepo: row.sourceRepo ?? void 0,
5195
+ tags: row.tags ?? [],
5196
+ createdAt: row.createdAt,
5197
+ updatedAt: row.updatedAt
5198
+ };
5199
+ }
5200
+ mapVersionRow(row) {
5201
+ return {
5202
+ id: row.id,
5203
+ specId: row.specId,
5204
+ semver: row.semver,
5205
+ content: row.content,
5206
+ checksum: row.checksum,
5207
+ gitRef: row.gitRef ?? void 0,
5208
+ pushedBy: row.pushedBy ?? void 0,
5209
+ compatibility: row.compatibility ?? void 0,
5210
+ previousVersion: row.previousVersion ?? void 0,
5211
+ forceReason: row.forceReason ?? void 0,
5212
+ isPrerelease: row.isPrerelease,
5213
+ createdAt: row.createdAt
5214
+ };
5215
+ }
5216
+ // GatewayConfigStore implementation
5217
+ async getGatewayConfig(name) {
5218
+ const rows = await this.db.select().from(gatewayConfigs2).where(eq2(gatewayConfigs2.name, name)).limit(1);
5219
+ if (rows.length === 0) return null;
5220
+ return this.mapGatewayConfigRow(rows[0]);
5221
+ }
5222
+ async listGatewayConfigs() {
5223
+ const rows = await this.db.select().from(gatewayConfigs2);
5224
+ return rows.map((r) => this.mapGatewayConfigRow(r));
5225
+ }
5226
+ async getGatewayConfigVersion(name, versionId) {
5227
+ const config = await this.getGatewayConfig(name);
5228
+ if (!config) return null;
5229
+ const rows = await this.db.select().from(gatewayConfigVersions2).where(and2(eq2(gatewayConfigVersions2.gatewayConfigId, config.id), eq2(gatewayConfigVersions2.id, versionId))).limit(1);
5230
+ if (rows.length === 0) return null;
5231
+ return this.mapGatewayConfigVersionRow(rows[0]);
5232
+ }
5233
+ async getLatestGatewayConfigVersion(name) {
5234
+ const config = await this.getGatewayConfig(name);
5235
+ if (!config) return null;
5236
+ const rows = await this.db.select().from(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.gatewayConfigId, config.id)).orderBy(desc2(gatewayConfigVersions2.createdAt), desc2(gatewayConfigVersions2.id)).limit(1);
5237
+ if (rows.length === 0) return null;
5238
+ return this.mapGatewayConfigVersionRow(rows[0]);
5239
+ }
5240
+ async listGatewayConfigVersions(name) {
5241
+ const config = await this.getGatewayConfig(name);
5242
+ if (!config) return [];
5243
+ const rows = await this.db.select().from(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.gatewayConfigId, config.id)).orderBy(desc2(gatewayConfigVersions2.createdAt), desc2(gatewayConfigVersions2.id)).limit(5);
5244
+ return rows.map((r) => this.mapGatewayConfigVersionRow(r));
5245
+ }
5246
+ async pushGatewayConfigVersion(config, version2) {
5247
+ const existingConfig = await this.getGatewayConfig(config.name);
5248
+ if (!existingConfig) {
5249
+ await this.db.insert(gatewayConfigs2).values({
5250
+ id: config.id,
5251
+ name: config.name,
5252
+ provider: config.provider,
5253
+ specName: config.specName,
5254
+ specSemver: config.specSemver,
5255
+ createdAt: config.createdAt,
5256
+ updatedAt: config.updatedAt
5257
+ });
5258
+ } else {
5259
+ await this.db.update(gatewayConfigs2).set({ updatedAt: /* @__PURE__ */ new Date(), specSemver: config.specSemver }).where(eq2(gatewayConfigs2.id, existingConfig.id));
5260
+ }
5261
+ const configId = existingConfig?.id ?? config.id;
5262
+ await this.db.insert(gatewayConfigVersions2).values({
5263
+ id: version2.id,
5264
+ gatewayConfigId: configId,
5265
+ routes: version2.routes,
5266
+ environments: version2.environments,
5267
+ callerIdentification: version2.callerIdentification ?? null,
5268
+ content: version2.content,
5269
+ checksum: version2.checksum,
5270
+ pushedBy: version2.pushedBy ?? null,
5271
+ createdAt: version2.createdAt
5272
+ });
5273
+ const versions = await this.db.select().from(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.gatewayConfigId, configId)).orderBy(desc2(gatewayConfigVersions2.createdAt), desc2(gatewayConfigVersions2.id));
5274
+ if (versions.length > 5) {
5275
+ const toDelete = versions.slice(5);
5276
+ for (const v of toDelete) {
5277
+ await this.db.delete(gatewayConfigVersions2).where(eq2(gatewayConfigVersions2.id, v.id));
5278
+ }
5279
+ }
5280
+ return version2;
5281
+ }
5282
+ async recordProvision(provision) {
5283
+ await this.db.insert(provisions2).values({
5284
+ id: provision.id,
5285
+ gatewayConfigName: provision.gatewayConfigName,
5286
+ gatewayConfigVersion: provision.gatewayConfigVersion,
5287
+ environment: provision.environment,
5288
+ provider: provision.provider,
5289
+ synced: provision.synced,
5290
+ actor: provision.actor,
5291
+ details: provision.details ?? null,
5292
+ createdAt: provision.createdAt
5293
+ });
5294
+ }
5295
+ async listProvisions(gatewayConfigName) {
5296
+ const rows = gatewayConfigName ? await this.db.select().from(provisions2).where(eq2(provisions2.gatewayConfigName, gatewayConfigName)).orderBy(desc2(provisions2.createdAt)) : await this.db.select().from(provisions2).orderBy(desc2(provisions2.createdAt));
5297
+ return rows.map((r) => ({
5298
+ id: r.id,
5299
+ gatewayConfigName: r.gatewayConfigName,
5300
+ gatewayConfigVersion: r.gatewayConfigVersion,
5301
+ environment: r.environment,
5302
+ provider: r.provider,
5303
+ synced: r.synced,
5304
+ actor: r.actor,
5305
+ details: r.details ?? void 0,
5306
+ createdAt: r.createdAt
5307
+ }));
5308
+ }
5309
+ mapGatewayConfigRow(row) {
5310
+ return {
5311
+ id: row.id,
5312
+ name: row.name,
5313
+ provider: row.provider,
5314
+ specName: row.specName,
5315
+ specSemver: row.specSemver,
5316
+ createdAt: row.createdAt,
5317
+ updatedAt: row.updatedAt
5318
+ };
5319
+ }
5320
+ mapGatewayConfigVersionRow(row) {
5321
+ return {
5322
+ id: row.id,
5323
+ gatewayConfigId: row.gatewayConfigId,
5324
+ routes: row.routes,
5325
+ environments: row.environments,
5326
+ callerIdentification: row.callerIdentification ?? void 0,
5327
+ content: row.content,
5328
+ checksum: row.checksum,
5329
+ pushedBy: row.pushedBy ?? void 0,
5330
+ createdAt: row.createdAt
5331
+ };
5332
+ }
5333
+ async recordGatewayLog(log) {
5334
+ await this.db.insert(httpLogs2).values({
5335
+ id: log.id,
5336
+ provider: log.provider,
5337
+ gatewayConfigName: log.gatewayConfigName,
5338
+ environment: log.environment,
5339
+ method: log.method,
5340
+ path: log.path,
5341
+ routePath: log.routePath ?? null,
5342
+ status: log.status,
5343
+ callerId: log.callerId ?? null,
5344
+ callerSource: log.callerSource ?? null,
5345
+ callerConfidence: log.callerConfidence,
5346
+ occurredAt: log.occurredAt,
5347
+ createdAt: log.createdAt
5348
+ });
5349
+ }
5350
+ async listGatewayLogs(filters) {
5351
+ const limit = filters.limit ?? 50;
5352
+ const offset = filters.offset ?? 0;
5353
+ let query = this.db.select().from(httpLogs2);
5354
+ const conditions = [];
5355
+ if (filters.gatewayConfigName) {
5356
+ conditions.push(eq2(httpLogs2.gatewayConfigName, filters.gatewayConfigName));
5357
+ }
5358
+ if (filters.environment) {
5359
+ conditions.push(eq2(httpLogs2.environment, filters.environment));
5360
+ }
5361
+ if (filters.path) {
5362
+ conditions.push(eq2(httpLogs2.path, filters.path));
5363
+ }
5364
+ if (filters.method) {
5365
+ conditions.push(eq2(httpLogs2.method, filters.method));
5366
+ }
5367
+ if (filters.status !== void 0) {
5368
+ conditions.push(eq2(httpLogs2.status, filters.status));
5369
+ }
5370
+ if (filters.from) {
5371
+ conditions.push(sql2`${httpLogs2.occurredAt} >= ${filters.from}`);
5372
+ }
5373
+ if (filters.to) {
5374
+ conditions.push(sql2`${httpLogs2.occurredAt} <= ${filters.to}`);
5375
+ }
5376
+ if (conditions.length > 0) {
5377
+ query = query.where(and2(...conditions));
5378
+ }
5379
+ const countResult = await this.db.select({ count: sql2`count(*)` }).from(httpLogs2).where(conditions.length > 0 ? and2(...conditions) : void 0);
5380
+ const total = countResult[0]?.count ?? 0;
5381
+ const rows = await query.orderBy(desc2(httpLogs2.occurredAt)).limit(limit).offset(offset);
5382
+ return {
5383
+ logs: rows.map((r) => ({
5384
+ id: r.id,
5385
+ provider: r.provider,
5386
+ gatewayConfigName: r.gatewayConfigName,
5387
+ environment: r.environment,
5388
+ method: r.method,
5389
+ path: r.path,
5390
+ routePath: r.routePath ?? void 0,
5391
+ status: r.status,
5392
+ callerId: r.callerId ?? void 0,
5393
+ callerSource: r.callerSource ?? void 0,
5394
+ callerConfidence: r.callerConfidence,
5395
+ occurredAt: r.occurredAt,
5396
+ createdAt: r.createdAt
5397
+ })),
5398
+ total
5399
+ };
5400
+ }
5401
+ async getGatewayLog(id) {
5402
+ const rows = await this.db.select().from(httpLogs2).where(eq2(httpLogs2.id, id)).limit(1);
5403
+ if (rows.length === 0) return null;
5404
+ const r = rows[0];
5405
+ return {
5406
+ id: r.id,
5407
+ provider: r.provider,
5408
+ gatewayConfigName: r.gatewayConfigName,
5409
+ environment: r.environment,
5410
+ method: r.method,
5411
+ path: r.path,
5412
+ routePath: r.routePath ?? void 0,
5413
+ status: r.status,
5414
+ callerId: r.callerId ?? void 0,
5415
+ callerSource: r.callerSource ?? void 0,
5416
+ callerConfidence: r.callerConfidence,
5417
+ occurredAt: r.occurredAt,
5418
+ createdAt: r.createdAt
5419
+ };
5420
+ }
5421
+ async getGatewayLogStats(_filters) {
5422
+ const conditions = [];
5423
+ if (_filters.gatewayConfigName) {
5424
+ conditions.push(eq2(httpLogs2.gatewayConfigName, _filters.gatewayConfigName));
5425
+ }
5426
+ if (_filters.environment) {
5427
+ conditions.push(eq2(httpLogs2.environment, _filters.environment));
5428
+ }
5429
+ if (_filters.from) {
5430
+ conditions.push(sql2`${httpLogs2.occurredAt} >= ${_filters.from}`);
5431
+ }
5432
+ if (_filters.to) {
5433
+ conditions.push(sql2`${httpLogs2.occurredAt} <= ${_filters.to}`);
5434
+ }
5435
+ const whereClause = conditions.length > 0 ? and2(...conditions) : void 0;
5436
+ const rows = await this.db.select({
5437
+ gatewayConfigName: httpLogs2.gatewayConfigName,
5438
+ environment: httpLogs2.environment,
5439
+ method: httpLogs2.method,
5440
+ routePath: httpLogs2.routePath,
5441
+ lastSeenAt: sql2`max(${httpLogs2.occurredAt})`,
5442
+ totalCalls: sql2`count(*)`,
5443
+ uniqueCallerIds: sql2`count(distinct ${httpLogs2.callerId})`
5444
+ }).from(httpLogs2).where(whereClause).groupBy(httpLogs2.gatewayConfigName, httpLogs2.environment, httpLogs2.method, httpLogs2.routePath);
5445
+ return rows.map((r) => ({
5446
+ gatewayConfigName: r.gatewayConfigName,
5447
+ environment: r.environment,
5448
+ method: r.method,
5449
+ routePath: r.routePath ?? "/",
5450
+ lastSeenAt: new Date(r.lastSeenAt),
5451
+ totalCalls: r.totalCalls,
5452
+ uniqueCallerIds: r.uniqueCallerIds
5453
+ }));
5454
+ }
5455
+ async deleteGatewayLogsOlderThan(days) {
5456
+ const cutoff = /* @__PURE__ */ new Date();
5457
+ cutoff.setDate(cutoff.getDate() - days);
5458
+ await this.db.delete(httpLogs2).where(sql2`${httpLogs2.occurredAt} < ${cutoff}`);
5459
+ }
5460
+ };
5461
+
4951
5462
  // src/registry/serve.ts
4952
5463
  async function startServer(userConfig) {
4953
5464
  const config = { ...defaultConfig, ...userConfig };
4954
- if (!config.sqlitePath && config.database === "sqlite") {
4955
- const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
4956
- config.sqlitePath = path9.join(homeDir, ".grapity", "registry.db");
4957
- }
4958
- if (config.database === "sqlite" && config.sqlitePath) {
4959
- const dir = path9.dirname(config.sqlitePath);
5465
+ let store;
5466
+ if (config.database === "postgresql") {
5467
+ if (!config.postgresUrl) {
5468
+ throw new Error("PostgreSQL database requested but no postgresUrl provided.");
5469
+ }
5470
+ store = new PostgreSQLSpecStore(config.postgresUrl);
5471
+ } else {
5472
+ const sqlitePath = config.sqlitePath ?? path9.join(
5473
+ process.env.HOME || process.env.USERPROFILE || ".",
5474
+ ".grapity",
5475
+ "registry.db"
5476
+ );
5477
+ const dir = path9.dirname(sqlitePath);
4960
5478
  if (!fs9.existsSync(dir)) {
4961
5479
  fs9.mkdirSync(dir, { recursive: true });
4962
5480
  }
5481
+ store = new SQLiteSpecStore(sqlitePath);
4963
5482
  }
4964
- const store = new SQLiteSpecStore(config.sqlitePath);
4965
5483
  await store.migrate();
4966
5484
  const app = createApp(config, store);
4967
- serve({
5485
+ const server = serve({
4968
5486
  fetch: app.fetch,
4969
5487
  port: config.port
4970
5488
  });
4971
- return app;
5489
+ return { app, store, server };
4972
5490
  }
4973
5491
  if (process.argv[1] === new URL(import.meta.url).pathname) {
4974
5492
  startServer();
@@ -5036,28 +5554,63 @@ function getPackageVersion() {
5036
5554
  return "unknown";
5037
5555
  }
5038
5556
  }
5557
+ function resolveServerConfig(cliOptions, cliConfig) {
5558
+ const envUrl = process.env.GRAPITY_DATABASE_URL;
5559
+ const dbValue = envUrl ?? cliOptions.db;
5560
+ if (dbValue) {
5561
+ if (isPostgresqlUrl(dbValue)) {
5562
+ return { port: cliOptions.port, database: "postgresql", postgresUrl: dbValue };
5563
+ }
5564
+ return { port: cliOptions.port, database: "sqlite", sqlitePath: dbValue };
5565
+ }
5566
+ if (cliConfig.mode === "local") {
5567
+ const local = cliConfig.local;
5568
+ if (local?.database === "postgresql") {
5569
+ if (!local.postgresUrl) {
5570
+ throw new Error(
5571
+ "PostgreSQL is configured but no postgresUrl is set. Run grapity init --local --db postgresql://... or set GRAPITY_DATABASE_URL."
5572
+ );
5573
+ }
5574
+ return { port: cliOptions.port, database: "postgresql", postgresUrl: local.postgresUrl };
5575
+ }
5576
+ return {
5577
+ port: cliOptions.port,
5578
+ database: "sqlite",
5579
+ sqlitePath: local?.sqlitePath ?? path11.join(os4.homedir(), ".grapity", "registry.db")
5580
+ };
5581
+ }
5582
+ return {
5583
+ port: cliOptions.port,
5584
+ database: "sqlite",
5585
+ sqlitePath: path11.join(os4.homedir(), ".grapity", "registry.db")
5586
+ };
5587
+ }
5039
5588
  function createServeCommand(_version) {
5040
- return new Command19("serve").description("Start the local grapity registry server").option("-p, --port <port>", "Port to listen on", "3750").option("--db <url>", "Database URL (sqlite path or postgresql URL)").option("--auth <mode>", "Auth mode: none, api-key, jwt", "none").option("--hub-port <port>", "Port for the developer portal (Hub)", "3000").option("--no-hub", "Skip starting the developer portal").action(async (options) => {
5589
+ return new Command19("serve").description("Start the local grapity registry server").option("-p, --port <port>", "Port to listen on", "3750").option("--hub-port <port>", "Port for the developer portal (Hub)", "3000").option("--no-hub", "Skip starting the developer portal").option("--db <path-or-url>", "SQLite path or postgresql:// URL").action(async (options) => {
5041
5590
  const port = parseInt(options.port, 10);
5042
5591
  const hubPort = parseInt(options.hubPort, 10);
5043
5592
  const startHub = options.hub !== false;
5044
- const db = options.db;
5045
- const auth = options.auth;
5046
- const isPostgres = db?.startsWith("postgresql://");
5047
- const dbMode = isPostgres ? "postgresql" : "sqlite";
5048
- const dbPath = isPostgres ? void 0 : db ?? path11.join(os4.homedir(), ".grapity", "registry.db");
5049
5593
  const version2 = getPackageVersion();
5594
+ const cliConfig = getConfig();
5595
+ let serverConfig;
5596
+ try {
5597
+ serverConfig = resolveServerConfig({ port, db: options.db }, cliConfig);
5598
+ } catch (err) {
5599
+ console.error(formatError("invalid config", err.message));
5600
+ process.exit(1);
5601
+ }
5050
5602
  console.log(formatHeader("grapity Registry", `v${version2}`));
5051
5603
  console.log("");
5052
- console.log(formatServeConfig({ mode: dbMode, port, dbPath, auth }));
5604
+ console.log(
5605
+ formatServeConfig({
5606
+ port,
5607
+ database: serverConfig.database,
5608
+ dbPath: serverConfig.sqlitePath,
5609
+ postgresUrl: serverConfig.postgresUrl
5610
+ })
5611
+ );
5053
5612
  console.log("");
5054
- await startServer({
5055
- port,
5056
- database: dbMode,
5057
- sqlitePath: dbPath,
5058
- postgresUrl: isPostgres ? db : void 0,
5059
- auth: auth === "none" ? { mode: "none" } : { mode: auth }
5060
- });
5613
+ const { store } = await startServer(serverConfig);
5061
5614
  console.log(formatReady(port));
5062
5615
  if (startHub) {
5063
5616
  console.log("");
@@ -5076,9 +5629,12 @@ function createServeCommand(_version) {
5076
5629
  });
5077
5630
  console.log(formatHubReady(hubPort));
5078
5631
  }
5079
- process.on("SIGINT", () => {
5632
+ process.on("SIGINT", async () => {
5080
5633
  console.log("");
5081
5634
  console.log(formatShutdown());
5635
+ if ("end" in store && typeof store.end === "function") {
5636
+ await store.end();
5637
+ }
5082
5638
  process.exit(0);
5083
5639
  });
5084
5640
  });