@secondlayer/subgraphs 3.2.0 → 3.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/src/index.d.ts +71 -9
  4. package/dist/src/index.js +461 -54
  5. package/dist/src/index.js.map +11 -9
  6. package/dist/src/runtime/block-processor.d.ts +37 -9
  7. package/dist/src/runtime/block-processor.js +127 -37
  8. package/dist/src/runtime/block-processor.js.map +5 -5
  9. package/dist/src/runtime/catchup.d.ts +34 -8
  10. package/dist/src/runtime/catchup.js +125 -36
  11. package/dist/src/runtime/catchup.js.map +5 -5
  12. package/dist/src/runtime/context.d.ts +27 -1
  13. package/dist/src/runtime/context.js +13 -2
  14. package/dist/src/runtime/context.js.map +3 -3
  15. package/dist/src/runtime/processor.js +143 -39
  16. package/dist/src/runtime/processor.js.map +8 -8
  17. package/dist/src/runtime/reindex.d.ts +34 -8
  18. package/dist/src/runtime/reindex.js +131 -36
  19. package/dist/src/runtime/reindex.js.map +6 -6
  20. package/dist/src/runtime/reorg.d.ts +34 -8
  21. package/dist/src/runtime/reorg.js +131 -39
  22. package/dist/src/runtime/reorg.js.map +6 -6
  23. package/dist/src/runtime/runner.d.ts +43 -9
  24. package/dist/src/runtime/source-matcher.d.ts +19 -10
  25. package/dist/src/runtime/source-matcher.js +26 -6
  26. package/dist/src/runtime/source-matcher.js.map +3 -3
  27. package/dist/src/schema/index.d.ts +42 -8
  28. package/dist/src/schema/index.js +57 -19
  29. package/dist/src/schema/index.js.map +5 -5
  30. package/dist/src/service.js +143 -39
  31. package/dist/src/service.js.map +8 -8
  32. package/dist/src/triggers/index.d.ts +16 -8
  33. package/dist/src/types.d.ts +35 -9
  34. package/dist/src/validate.d.ts +34 -8
  35. package/dist/src/validate.js +10 -3
  36. package/dist/src/validate.js.map +3 -3
  37. package/package.json +10 -3
package/dist/src/index.js CHANGED
@@ -23,7 +23,13 @@ var SubgraphColumnSchema = z.object({
23
23
  var SubgraphTableSchema = z.object({
24
24
  columns: z.record(z.string(), SubgraphColumnSchema).refine((c) => Object.keys(c).length > 0, "Table must have at least one column"),
25
25
  indexes: z.array(z.array(z.string())).optional(),
26
- uniqueKeys: z.array(z.array(z.string())).optional()
26
+ uniqueKeys: z.array(z.array(z.string())).optional(),
27
+ relations: z.array(z.object({
28
+ name: z.string(),
29
+ references: z.string(),
30
+ fields: z.array(z.string()).min(1),
31
+ referencedColumns: z.array(z.string()).min(1)
32
+ })).optional()
27
33
  });
28
34
  var SubgraphSchemaSchema = z.record(z.string(), SubgraphTableSchema).refine((s) => Object.keys(s).length > 0, "Schema must have at least one table");
29
35
  var VALID_FILTER_TYPES = [
@@ -55,7 +61,8 @@ var SubgraphFilterSchema = z.object({
55
61
  contractName: z.string().optional(),
56
62
  topic: z.string().optional(),
57
63
  lockedAddress: z.string().optional(),
58
- abi: z.record(z.string(), z.any()).optional()
64
+ abi: z.record(z.string(), z.any()).optional(),
65
+ trait: z.string().optional()
59
66
  }).strict();
60
67
  var SubgraphDefinitionSchema = z.object({
61
68
  name: SubgraphNameSchema,
@@ -87,12 +94,14 @@ class SubgraphContext {
87
94
  pgSchemaName;
88
95
  subgraphSchema;
89
96
  ops = [];
90
- constructor(db, pgSchemaName, subgraphSchema, block, tx) {
97
+ byo;
98
+ constructor(db, pgSchemaName, subgraphSchema, block, tx, byo = false) {
91
99
  this.db = db;
92
100
  this.pgSchemaName = pgSchemaName;
93
101
  this.subgraphSchema = subgraphSchema;
94
102
  this.block = block;
95
103
  this._tx = tx;
104
+ this.byo = byo;
96
105
  }
97
106
  get tx() {
98
107
  return this._tx;
@@ -287,6 +296,15 @@ class SubgraphContext {
287
296
  }
288
297
  buildStatements(ops) {
289
298
  const statements = [];
299
+ if (this.byo) {
300
+ const insertTables = new Set;
301
+ for (const op of ops)
302
+ if (op.kind === "insert")
303
+ insertTables.add(op.table);
304
+ for (const table of insertTables) {
305
+ statements.push(`DELETE FROM "${this.pgSchemaName}"."${table}" WHERE "_block_height" = ${this.block.height}`);
306
+ }
307
+ }
290
308
  let currentBatch = null;
291
309
  let currentBatchKey = "";
292
310
  const flushInsertBatch = () => {
@@ -780,7 +798,19 @@ function matchPattern(value, pattern) {
780
798
  }
781
799
  return re.test(value);
782
800
  }
783
- function matchFilter(filter, transactions, eventsByTx) {
801
+ var EMPTY_SET = new Set;
802
+ function traitAllows(filter, contractId, traitContracts) {
803
+ const trait = filter.trait;
804
+ if (!trait)
805
+ return true;
806
+ if (!contractId)
807
+ return false;
808
+ return (traitContracts.get(trait) ?? EMPTY_SET).has(contractId);
809
+ }
810
+ function assetContract(assetId) {
811
+ return assetId?.split("::")[0];
812
+ }
813
+ function matchFilter(filter, transactions, eventsByTx, traitContracts) {
784
814
  const results = [];
785
815
  switch (filter.type) {
786
816
  case "stx_transfer":
@@ -844,6 +874,8 @@ function matchFilter(filter, transactions, eventsByTx) {
844
874
  if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
845
875
  return false;
846
876
  }
877
+ if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
878
+ return false;
847
879
  if ("sender" in filter && filter.sender) {
848
880
  if (!matchPattern(data.sender, filter.sender))
849
881
  return false;
@@ -882,6 +914,8 @@ function matchFilter(filter, transactions, eventsByTx) {
882
914
  if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
883
915
  return false;
884
916
  }
917
+ if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
918
+ return false;
885
919
  if ("sender" in filter && filter.sender) {
886
920
  if (!matchPattern(data.sender, filter.sender))
887
921
  return false;
@@ -914,6 +948,8 @@ function matchFilter(filter, transactions, eventsByTx) {
914
948
  if (!matchPattern(tx.sender, filter.caller))
915
949
  continue;
916
950
  }
951
+ if (!traitAllows(filter, tx.contract_id, traitContracts))
952
+ continue;
917
953
  const txEvents = eventsByTx.get(tx.tx_id) ?? [];
918
954
  results.push({ tx, events: txEvents });
919
955
  }
@@ -948,11 +984,13 @@ function matchFilter(filter, transactions, eventsByTx) {
948
984
  return false;
949
985
  if (data.topic !== "print")
950
986
  return false;
987
+ const printContractId = data.contract_identifier ?? data.contract_id;
951
988
  if (filter.contractId) {
952
- const contractId = data.contract_identifier ?? data.contract_id;
953
- if (!contractId || !matchPattern(contractId, filter.contractId))
989
+ if (!printContractId || !matchPattern(printContractId, filter.contractId))
954
990
  return false;
955
991
  }
992
+ if (!traitAllows(filter, printContractId, traitContracts))
993
+ return false;
956
994
  return true;
957
995
  });
958
996
  if (matched.length > 0) {
@@ -964,7 +1002,7 @@ function matchFilter(filter, transactions, eventsByTx) {
964
1002
  }
965
1003
  return results;
966
1004
  }
967
- function matchSources(sources, transactions, events) {
1005
+ function matchSources(sources, transactions, events, traitContracts = new Map) {
968
1006
  const eventsByTx = new Map;
969
1007
  for (const event of events) {
970
1008
  const list = eventsByTx.get(event.tx_id) ?? [];
@@ -974,7 +1012,7 @@ function matchSources(sources, transactions, events) {
974
1012
  const seen = new Set;
975
1013
  const results = [];
976
1014
  for (const [sourceName, filter] of Object.entries(sources)) {
977
- const matches = matchFilter(filter, transactions, eventsByTx);
1015
+ const matches = matchFilter(filter, transactions, eventsByTx, traitContracts);
978
1016
  for (const match of matches) {
979
1017
  const dedupeKey = `${match.tx.tx_id}:${sourceName}`;
980
1018
  if (!seen.has(dedupeKey)) {
@@ -991,8 +1029,11 @@ import {
991
1029
  getSourceDb,
992
1030
  getTargetDb
993
1031
  } from "@secondlayer/shared/db";
1032
+ import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contracts";
994
1033
  import {
1034
+ isByoSubgraph,
995
1035
  recordSubgraphProcessed,
1036
+ resolveSubgraphDb,
996
1037
  updateSubgraphStatus
997
1038
  } from "@secondlayer/shared/db/queries/subgraphs";
998
1039
  import { logger as logger4 } from "@secondlayer/shared/logger";
@@ -1230,7 +1271,38 @@ async function refreshMatcher(db) {
1230
1271
  }
1231
1272
 
1232
1273
  // src/runtime/block-processor.ts
1233
- var schemaNameCache = new Map;
1274
+ var routeCache = new Map;
1275
+ async function resolveRoute(subgraphName, targetDb) {
1276
+ const cached = routeCache.get(subgraphName);
1277
+ if (cached)
1278
+ return cached;
1279
+ const row = await targetDb.selectFrom("subgraphs").selectAll().where("name", "=", subgraphName).executeTakeFirst();
1280
+ const byo = row ? isByoSubgraph(row) : false;
1281
+ const route = {
1282
+ schemaName: row?.schema_name ?? pgSchemaName(subgraphName),
1283
+ dataDb: row && byo ? resolveSubgraphDb(row) : targetDb,
1284
+ byo
1285
+ };
1286
+ routeCache.set(subgraphName, route);
1287
+ return route;
1288
+ }
1289
+ function invalidateSubgraphRoute(subgraphName) {
1290
+ routeCache.delete(subgraphName);
1291
+ }
1292
+ async function resolveTraitContracts(subgraph, blockHeight, db) {
1293
+ const traits = new Set;
1294
+ for (const source of Object.values(subgraph.sources)) {
1295
+ const trait = source.trait;
1296
+ if (trait)
1297
+ traits.add(trait);
1298
+ }
1299
+ const resolved = new Map;
1300
+ for (const trait of traits) {
1301
+ const ids = await resolveTraitContractIds(db, trait, blockHeight);
1302
+ resolved.set(trait, new Set(ids));
1303
+ }
1304
+ return resolved;
1305
+ }
1234
1306
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1235
1307
  const sourceDb = getSourceDb();
1236
1308
  const targetDb = getTargetDb();
@@ -1270,7 +1342,8 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1270
1342
  txs = await sourceDb.selectFrom("transactions").selectAll().where("block_height", "=", blockHeight).execute();
1271
1343
  evts = await sourceDb.selectFrom("events").selectAll().where("block_height", "=", blockHeight).execute();
1272
1344
  }
1273
- const matched = matchSources(subgraph.sources, txs, evts);
1345
+ const traitContracts = await resolveTraitContracts(subgraph, blockHeight, targetDb);
1346
+ const matched = matchSources(subgraph.sources, txs, evts, traitContracts);
1274
1347
  result.matched = matched.length;
1275
1348
  if (matched.length === 0) {
1276
1349
  if (!opts?.skipProgressUpdate) {
@@ -1278,12 +1351,8 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1278
1351
  }
1279
1352
  return result;
1280
1353
  }
1281
- let schemaName = schemaNameCache.get(subgraphName);
1282
- if (!schemaName) {
1283
- const subgraphRecord = await targetDb.selectFrom("subgraphs").select("schema_name").where("name", "=", subgraphName).executeTakeFirst();
1284
- schemaName = subgraphRecord?.schema_name ?? pgSchemaName(subgraphName);
1285
- schemaNameCache.set(subgraphName, schemaName);
1286
- }
1354
+ const route = await resolveRoute(subgraphName, targetDb);
1355
+ const schemaName = route.schemaName;
1287
1356
  const blockMeta = {
1288
1357
  height: block.height,
1289
1358
  hash: block.hash,
@@ -1298,30 +1367,57 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1298
1367
  };
1299
1368
  let handlerMs = 0;
1300
1369
  let flushMs = 0;
1301
- await targetDb.transaction().execute(async (tx) => {
1302
- const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx);
1303
- const handlerStart = performance.now();
1304
- const runResult = await runHandlers(subgraph, matched, ctx);
1305
- handlerMs = performance.now() - handlerStart;
1370
+ const applyProgress = async (tx, rr) => {
1371
+ if (opts?.skipProgressUpdate)
1372
+ return;
1373
+ const status = rr.errors > 0 && rr.processed === 0 ? "error" : "active";
1374
+ await updateSubgraphStatus(tx, subgraphName, status, blockHeight);
1375
+ if (rr.processed > 0 || rr.errors > 0) {
1376
+ const lastError = rr.errors > 0 ? `${rr.errors} error(s) at block ${blockHeight}` : undefined;
1377
+ await recordSubgraphProcessed(tx, subgraphName, rr.processed, rr.errors, lastError);
1378
+ }
1379
+ };
1380
+ if (route.byo) {
1381
+ let runResult = { processed: 0, errors: 0 };
1382
+ let manifest;
1383
+ await route.dataDb.transaction().execute(async (tx) => {
1384
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true);
1385
+ const handlerStart = performance.now();
1386
+ runResult = await runHandlers(subgraph, matched, ctx);
1387
+ handlerMs = performance.now() - handlerStart;
1388
+ if (ctx.pendingOps > 0) {
1389
+ const flushStart = performance.now();
1390
+ manifest = await ctx.flush();
1391
+ flushMs = performance.now() - flushStart;
1392
+ }
1393
+ });
1306
1394
  result.processed = runResult.processed;
1307
1395
  result.errors = runResult.errors;
1308
- if (ctx.pendingOps > 0) {
1309
- const flushStart = performance.now();
1310
- const manifest = await ctx.flush();
1311
- if (manifest.count > 0) {
1396
+ await targetDb.transaction().execute(async (tx) => {
1397
+ if (manifest && manifest.count > 0) {
1312
1398
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1313
1399
  }
1314
- flushMs = performance.now() - flushStart;
1315
- }
1316
- if (!opts?.skipProgressUpdate) {
1317
- const status = runResult.errors > 0 && runResult.processed === 0 ? "error" : "active";
1318
- await updateSubgraphStatus(tx, subgraphName, status, blockHeight);
1319
- }
1320
- if (!opts?.skipProgressUpdate && (runResult.processed > 0 || runResult.errors > 0)) {
1321
- const lastError = runResult.errors > 0 ? `${runResult.errors} error(s) at block ${blockHeight}` : undefined;
1322
- await recordSubgraphProcessed(tx, subgraphName, runResult.processed, runResult.errors, lastError);
1323
- }
1324
- });
1400
+ await applyProgress(tx, runResult);
1401
+ });
1402
+ } else {
1403
+ await targetDb.transaction().execute(async (tx) => {
1404
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx);
1405
+ const handlerStart = performance.now();
1406
+ const runResult = await runHandlers(subgraph, matched, ctx);
1407
+ handlerMs = performance.now() - handlerStart;
1408
+ result.processed = runResult.processed;
1409
+ result.errors = runResult.errors;
1410
+ if (ctx.pendingOps > 0) {
1411
+ const flushStart = performance.now();
1412
+ const manifest = await ctx.flush();
1413
+ if (manifest.count > 0) {
1414
+ await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1415
+ }
1416
+ flushMs = performance.now() - flushStart;
1417
+ }
1418
+ await applyProgress(tx, runResult);
1419
+ });
1420
+ }
1325
1421
  const totalMs = performance.now() - blockStart;
1326
1422
  result.timing = {
1327
1423
  totalMs: Math.round(totalMs),
@@ -1332,7 +1428,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1332
1428
  try {
1333
1429
  const tables = Object.keys(subgraph.schema);
1334
1430
  for (const table of tables) {
1335
- const { rows } = await sql3.raw(`SELECT n_live_tup AS count FROM pg_stat_user_tables WHERE schemaname = '${schemaName}' AND relname = '${table}'`).execute(targetDb);
1431
+ const { rows } = await sql3.raw(`SELECT n_live_tup AS count FROM pg_stat_user_tables WHERE schemaname = '${schemaName}' AND relname = '${table}'`).execute(route.dataDb);
1336
1432
  const count = Number(rows[0]?.count ?? 0);
1337
1433
  if (count >= 1e7) {
1338
1434
  logger4.warn("Subgraph table exceeds 10M rows (estimate)", {
@@ -1504,6 +1600,12 @@ function generateSubgraphSQL(def, schemaNameOverride) {
1504
1600
  }
1505
1601
  }
1506
1602
  }
1603
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
1604
+ for (const rel of tableDef.relations ?? []) {
1605
+ const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
1606
+ statements.push(`ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`);
1607
+ }
1608
+ }
1507
1609
  const hashInput = JSON.stringify({
1508
1610
  name: def.name,
1509
1611
  schema: def.schema,
@@ -1968,6 +2070,283 @@ async function backfillSubgraph(def, opts) {
1968
2070
  function defineSubgraph(def) {
1969
2071
  return def;
1970
2072
  }
2073
+ // src/schema/prisma.ts
2074
+ var PRISMA_TYPE = {
2075
+ uint: { type: "Decimal", db: "@db.Numeric" },
2076
+ int: { type: "Decimal", db: "@db.Numeric" },
2077
+ text: { type: "String" },
2078
+ principal: { type: "String" },
2079
+ boolean: { type: "Boolean" },
2080
+ timestamp: { type: "DateTime", db: "@db.Timestamptz" },
2081
+ jsonb: { type: "Json" }
2082
+ };
2083
+ var SYSTEM_FIELDS = [
2084
+ { field: "id", col: "_id", line: "BigInt @id @default(autoincrement())" },
2085
+ { field: "blockHeight", col: "_block_height", line: "BigInt" },
2086
+ { field: "txId", col: "_tx_id", line: "String" },
2087
+ {
2088
+ field: "createdAt",
2089
+ col: "_created_at",
2090
+ line: "DateTime @default(now()) @db.Timestamptz"
2091
+ }
2092
+ ];
2093
+ function snakeToCamel(name) {
2094
+ return name.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
2095
+ }
2096
+ function pascalCase(name) {
2097
+ const camel = snakeToCamel(name);
2098
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
2099
+ }
2100
+ function quotePrismaDefault(value) {
2101
+ if (typeof value === "string")
2102
+ return `@default("${value.replace(/"/g, "\\\"")}")`;
2103
+ return `@default(${value})`;
2104
+ }
2105
+ function renderTable(tableName, table, backRelations) {
2106
+ const lines = [];
2107
+ for (const sys of SYSTEM_FIELDS) {
2108
+ lines.push(` ${sys.field} ${sys.line} @map("${sys.col}")`);
2109
+ }
2110
+ for (const [colName, col] of Object.entries(table.columns)) {
2111
+ const field = snakeToCamel(colName);
2112
+ const mapped = PRISMA_TYPE[col.type];
2113
+ const optional = col.nullable ? "?" : "";
2114
+ const attrs = [];
2115
+ if (field !== colName)
2116
+ attrs.push(`@map("${colName}")`);
2117
+ if (col.default !== undefined)
2118
+ attrs.push(quotePrismaDefault(col.default));
2119
+ if (mapped.db)
2120
+ attrs.push(mapped.db);
2121
+ lines.push(` ${field} ${mapped.type}${optional}${attrs.length ? ` ${attrs.join(" ")}` : ""}`);
2122
+ }
2123
+ for (const rel of table.relations ?? []) {
2124
+ const optional = rel.fields.some((f) => table.columns[f]?.nullable) ? "?" : "";
2125
+ const relName = `${pascalCase(tableName)}_${rel.name}`;
2126
+ const fields = rel.fields.map(snakeToCamel).join(", ");
2127
+ const refs = rel.referencedColumns.map(snakeToCamel).join(", ");
2128
+ lines.push(` ${rel.name} ${pascalCase(rel.references)}${optional} @relation("${relName}", fields: [${fields}], references: [${refs}])`);
2129
+ }
2130
+ for (const back of backRelations) {
2131
+ lines.push(` ${back.field} ${back.model}[] @relation("${back.relationName}")`);
2132
+ }
2133
+ const block = [];
2134
+ for (const [colName, col] of Object.entries(table.columns)) {
2135
+ if (col.indexed)
2136
+ block.push(` @@index([${snakeToCamel(colName)}])`);
2137
+ }
2138
+ for (const cols of table.indexes ?? []) {
2139
+ block.push(` @@index([${cols.map(snakeToCamel).join(", ")}])`);
2140
+ }
2141
+ for (const cols of table.uniqueKeys ?? []) {
2142
+ block.push(` @@unique([${cols.map(snakeToCamel).join(", ")}])`);
2143
+ }
2144
+ block.push(` @@map("${tableName}")`);
2145
+ return `model ${pascalCase(tableName)} {
2146
+ ${lines.join(`
2147
+ `)}
2148
+
2149
+ ${block.join(`
2150
+ `)}
2151
+ }`;
2152
+ }
2153
+ function computeBackRelations(schema) {
2154
+ const map = new Map;
2155
+ for (const [owningTable, table] of Object.entries(schema)) {
2156
+ for (const rel of table.relations ?? []) {
2157
+ const list = map.get(rel.references) ?? [];
2158
+ list.push({
2159
+ field: `${snakeToCamel(owningTable)}${pascalCase(rel.name)}`,
2160
+ model: pascalCase(owningTable),
2161
+ relationName: `${pascalCase(owningTable)}_${rel.name}`
2162
+ });
2163
+ map.set(rel.references, list);
2164
+ }
2165
+ }
2166
+ return map;
2167
+ }
2168
+ function generatePrismaSchema(def, opts = {}) {
2169
+ const schemaName = opts.schemaName ?? pgSchemaName(def.name);
2170
+ const env = opts.datasourceEnv ?? "DATABASE_URL";
2171
+ const header = [
2172
+ `// Generated by \`sl generate prisma\` from subgraph "${def.name}". Do not edit by hand.`,
2173
+ "",
2174
+ "datasource db {",
2175
+ ' provider = "postgresql"',
2176
+ ` url = env("${env}")`,
2177
+ ` schemas = ["${schemaName}"]`,
2178
+ "}",
2179
+ "",
2180
+ "generator client {",
2181
+ ' provider = "prisma-client-js"',
2182
+ ' previewFeatures = ["multiSchema"]',
2183
+ "}"
2184
+ ].join(`
2185
+ `);
2186
+ const backRelations = computeBackRelations(def.schema);
2187
+ const modelBlocks = Object.entries(def.schema).map(([tableName, table]) => {
2188
+ const body = renderTable(tableName, table, backRelations.get(tableName) ?? []);
2189
+ return body.replace(/\n}$/, `
2190
+ @@schema("${schemaName}")
2191
+ }`);
2192
+ });
2193
+ if (opts.modelsOnly)
2194
+ return `${modelBlocks.join(`
2195
+
2196
+ `)}
2197
+ `;
2198
+ return `${header}
2199
+
2200
+ ${modelBlocks.join(`
2201
+
2202
+ `)}
2203
+ `;
2204
+ }
2205
+ // src/schema/drizzle.ts
2206
+ var DRIZZLE_BUILDER = {
2207
+ uint: (c) => `numeric("${c}")`,
2208
+ int: (c) => `numeric("${c}")`,
2209
+ text: (c) => `text("${c}")`,
2210
+ principal: (c) => `text("${c}")`,
2211
+ boolean: (c) => `boolean("${c}")`,
2212
+ timestamp: (c) => `timestamp("${c}", { withTimezone: true })`,
2213
+ jsonb: (c) => `jsonb("${c}")`
2214
+ };
2215
+ var BUILDERS_USED = {
2216
+ uint: "numeric",
2217
+ int: "numeric",
2218
+ text: "text",
2219
+ principal: "text",
2220
+ boolean: "boolean",
2221
+ timestamp: "timestamp",
2222
+ jsonb: "jsonb"
2223
+ };
2224
+ function snakeToCamel2(name) {
2225
+ return name.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
2226
+ }
2227
+ function pascalCase2(name) {
2228
+ const camel = snakeToCamel2(name);
2229
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
2230
+ }
2231
+ function literalDefault(value) {
2232
+ return typeof value === "string" ? `"${value.replace(/"/g, "\\\"")}"` : `${value}`;
2233
+ }
2234
+ function renderTable2(varName, tableName, table) {
2235
+ const cols = [
2236
+ ` id: bigserial("_id", { mode: "bigint" }).primaryKey(),`,
2237
+ ` blockHeight: bigint("_block_height", { mode: "bigint" }).notNull(),`,
2238
+ ` txId: text("_tx_id").notNull(),`,
2239
+ ` createdAt: timestamp("_created_at", { withTimezone: true }).notNull().defaultNow(),`
2240
+ ];
2241
+ for (const [colName, col] of Object.entries(table.columns)) {
2242
+ let line = ` ${snakeToCamel2(colName)}: ${DRIZZLE_BUILDER[col.type](colName)}`;
2243
+ if (!col.nullable)
2244
+ line += ".notNull()";
2245
+ if (col.default !== undefined)
2246
+ line += `.default(${literalDefault(col.default)})`;
2247
+ cols.push(`${line},`);
2248
+ }
2249
+ const extras = [];
2250
+ for (const [colName, col] of Object.entries(table.columns)) {
2251
+ if (col.indexed) {
2252
+ const f = snakeToCamel2(colName);
2253
+ extras.push(` ${f}Idx: index("idx_${tableName}_${colName}").on(t.${f})`);
2254
+ }
2255
+ }
2256
+ table.indexes?.forEach((idxCols, i) => {
2257
+ const on = idxCols.map((c) => `t.${snakeToCamel2(c)}`).join(", ");
2258
+ extras.push(` idx${i}: index("idx_${tableName}_${i}").on(${on})`);
2259
+ });
2260
+ table.uniqueKeys?.forEach((uqCols, i) => {
2261
+ const on = uqCols.map((c) => `t.${snakeToCamel2(c)}`).join(", ");
2262
+ extras.push(` uq${i}: uniqueIndex("uq_${tableName}_${i}").on(${on})`);
2263
+ });
2264
+ const extrasBlock = extras.length ? `, (t) => ({
2265
+ ${extras.join(`,
2266
+ `)},
2267
+ })` : "";
2268
+ return `export const ${varName} = sg.table("${tableName}", {
2269
+ ${cols.join(`
2270
+ `)}
2271
+ }${extrasBlock});`;
2272
+ }
2273
+ function renderRelations(schema) {
2274
+ const back = new Map;
2275
+ for (const [owning, table] of Object.entries(schema)) {
2276
+ for (const rel of table.relations ?? []) {
2277
+ const list = back.get(rel.references) ?? [];
2278
+ list.push({
2279
+ field: `${snakeToCamel2(owning)}${pascalCase2(rel.name)}`,
2280
+ from: snakeToCamel2(owning)
2281
+ });
2282
+ back.set(rel.references, list);
2283
+ }
2284
+ }
2285
+ const out = [];
2286
+ for (const [tableName, table] of Object.entries(schema)) {
2287
+ const v = snakeToCamel2(tableName);
2288
+ const ones = (table.relations ?? []).map((rel) => {
2289
+ const localFields = rel.fields.map((f) => `${v}.${snakeToCamel2(f)}`).join(", ");
2290
+ const refFields = rel.referencedColumns.map((c) => `${snakeToCamel2(rel.references)}.${snakeToCamel2(c)}`).join(", ");
2291
+ return ` ${rel.name}: one(${snakeToCamel2(rel.references)}, { fields: [${localFields}], references: [${refFields}] }),`;
2292
+ });
2293
+ const manys = (back.get(tableName) ?? []).map((b) => ` ${b.field}: many(${b.from}),`);
2294
+ if (ones.length === 0 && manys.length === 0)
2295
+ continue;
2296
+ const helpers = [ones.length ? "one" : "", manys.length ? "many" : ""].filter(Boolean).join(", ");
2297
+ out.push(`export const ${v}Relations = relations(${v}, ({ ${helpers} }) => ({
2298
+ ${[...ones, ...manys].join(`
2299
+ `)}
2300
+ }));`);
2301
+ }
2302
+ return out;
2303
+ }
2304
+ function generateDrizzleSchema(def, opts = {}) {
2305
+ const schemaName = opts.schemaName ?? pgSchemaName(def.name);
2306
+ const used = new Set(["bigserial", "bigint", "text", "timestamp"]);
2307
+ let needsIndex = false;
2308
+ let needsUnique = false;
2309
+ for (const table of Object.values(def.schema)) {
2310
+ for (const col of Object.values(table.columns))
2311
+ used.add(BUILDERS_USED[col.type]);
2312
+ if (Object.values(table.columns).some((c) => c.indexed) || table.indexes?.length)
2313
+ needsIndex = true;
2314
+ if (table.uniqueKeys?.length)
2315
+ needsUnique = true;
2316
+ }
2317
+ if (needsIndex)
2318
+ used.add("index");
2319
+ if (needsUnique)
2320
+ used.add("uniqueIndex");
2321
+ used.add("pgSchema");
2322
+ const hasRelations = Object.values(def.schema).some((t) => t.relations?.length);
2323
+ const imports = [
2324
+ `import { ${[...used].sort().join(", ")} } from "drizzle-orm/pg-core";`,
2325
+ ...hasRelations ? [`import { relations } from "drizzle-orm";`] : []
2326
+ ].join(`
2327
+ `);
2328
+ const tables = Object.entries(def.schema).map(([name, table]) => renderTable2(snakeToCamel2(name), name, table));
2329
+ const relationDecls = renderRelations(def.schema);
2330
+ const typeExports = Object.keys(def.schema).map((name) => `export type ${pascalCase2(name)} = typeof ${snakeToCamel2(name)}.$inferSelect;`);
2331
+ return [
2332
+ `// Generated by \`sl generate drizzle\` from subgraph "${def.name}". Do not edit by hand.`,
2333
+ imports,
2334
+ "",
2335
+ `export const sg = pgSchema("${schemaName}");`,
2336
+ "",
2337
+ tables.join(`
2338
+
2339
+ `),
2340
+ ...relationDecls.length ? ["", relationDecls.join(`
2341
+
2342
+ `)] : [],
2343
+ "",
2344
+ typeExports.join(`
2345
+ `),
2346
+ ""
2347
+ ].join(`
2348
+ `);
2349
+ }
1971
2350
  // src/schema/deployer.ts
1972
2351
  import { sql as sql4 } from "kysely";
1973
2352
  function toJsonSafe(obj) {
@@ -2021,10 +2400,29 @@ function bumpPatch(version) {
2021
2400
  const patch = Number.parseInt(parts[2] ?? "0", 10);
2022
2401
  return `${parts[0]}.${parts[1]}.${Number.isNaN(patch) ? 1 : patch + 1}`;
2023
2402
  }
2403
+ function renderDeployPlan(def, schemaName) {
2404
+ validateSubgraphDefinition(def);
2405
+ const { statements } = generateSubgraphSQL(def, schemaName);
2406
+ const schema = schemaName ?? pgSchemaName(def.name);
2407
+ const grantScript = [
2408
+ "-- Run once on YOUR database as an owner/superuser, replacing <role>",
2409
+ "-- with the role whose credentials you give Secondlayer.",
2410
+ "-- Secondlayer then creates and owns only this one schema:",
2411
+ `GRANT CREATE ON DATABASE current_database() TO <role>;`,
2412
+ `-- (after first deploy <role> owns "${schema}"; no further grants needed)`
2413
+ ].join(`
2414
+ `);
2415
+ return { schemaName: schema, statements, grantScript };
2416
+ }
2024
2417
  async function deploySchema(db, def, handlerPath, opts) {
2025
2418
  validateSubgraphDefinition(def);
2026
2419
  const { statements, hash } = generateSubgraphSQL(def, opts?.schemaName);
2027
2420
  const { getSubgraph, registerSubgraph } = await import("@secondlayer/shared/db/queries/subgraphs");
2421
+ const ddlDb = opts?.dataDb ?? db;
2422
+ const byo = opts?.dataDb != null;
2423
+ const refuseDestructiveOnByo = (reason) => {
2424
+ throw new Error(`Breaking schema change on a BYO subgraph (${reason}) would drop data in your database. Drop the schema "${opts?.schemaName ?? pgSchemaName(def.name)}" manually and re-deploy to rebuild.`);
2425
+ };
2028
2426
  const existing = await getSubgraph(db, def.name, opts?.accountId);
2029
2427
  const schemaName = opts?.schemaName ?? pgSchemaName(def.name);
2030
2428
  const newVersion = opts?.version ?? (existing ? bumpPatch(existing.version) : "1.0.0");
@@ -2046,7 +2444,8 @@ async function deploySchema(db, def, handlerPath, opts) {
2046
2444
  handlerCode: opts?.handlerCode,
2047
2445
  sourceCode: opts?.sourceCode,
2048
2446
  schemaName,
2049
- startBlock: def.startBlock
2447
+ startBlock: def.startBlock,
2448
+ databaseUrlEnc: opts?.databaseUrlEnc ?? null
2050
2449
  };
2051
2450
  if (existing) {
2052
2451
  const schemaExists = await sql4`
@@ -2054,10 +2453,10 @@ async function deploySchema(db, def, handlerPath, opts) {
2054
2453
  SELECT 1 FROM information_schema.schemata
2055
2454
  WHERE schema_name = ${schemaName}
2056
2455
  ) AS "exists"
2057
- `.execute(db).then((r) => r.rows[0]?.exists ?? false);
2456
+ `.execute(ddlDb).then((r) => r.rows[0]?.exists ?? false);
2058
2457
  if (!schemaExists) {
2059
2458
  for (const stmt of statements) {
2060
- await sql4.raw(stmt).execute(db);
2459
+ await sql4.raw(stmt).execute(ddlDb);
2061
2460
  }
2062
2461
  const sg2 = await registerSubgraph(db, regData);
2063
2462
  return { action: "reindexed", subgraphId: sg2.id, version: newVersion };
@@ -2076,9 +2475,11 @@ async function deploySchema(db, def, handlerPath, opts) {
2076
2475
  };
2077
2476
  }
2078
2477
  if (existing.schema_hash === hash && opts?.forceReindex) {
2079
- await sql4.raw(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`).execute(db);
2478
+ if (byo)
2479
+ refuseDestructiveOnByo("force reindex");
2480
+ await sql4.raw(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`).execute(ddlDb);
2080
2481
  for (const stmt of statements) {
2081
- await sql4.raw(stmt).execute(db);
2482
+ await sql4.raw(stmt).execute(ddlDb);
2082
2483
  }
2083
2484
  const sg2 = await registerSubgraph(db, regData);
2084
2485
  return { action: "reindexed", subgraphId: sg2.id, version: newVersion };
@@ -2087,9 +2488,12 @@ async function deploySchema(db, def, handlerPath, opts) {
2087
2488
  const diff = diffSchema(existing.definition.schema, def.schema);
2088
2489
  const { breaking, reasons } = hasBreakingChanges(diff);
2089
2490
  if (breaking || opts?.forceReindex) {
2090
- await sql4.raw(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`).execute(db);
2491
+ if (byo) {
2492
+ refuseDestructiveOnByo(reasons.length > 0 ? reasons.join("; ") : "force reindex");
2493
+ }
2494
+ await sql4.raw(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`).execute(ddlDb);
2091
2495
  for (const stmt of statements) {
2092
- await sql4.raw(stmt).execute(db);
2496
+ await sql4.raw(stmt).execute(ddlDb);
2093
2497
  }
2094
2498
  const sg3 = await registerSubgraph(db, regData);
2095
2499
  const deployDiff2 = {
@@ -2126,15 +2530,15 @@ async function deploySchema(db, def, handlerPath, opts) {
2126
2530
  await sql4.raw(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
2127
2531
  ${colDefs.join(`,
2128
2532
  `)}
2129
- )`).execute(db);
2130
- await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`).execute(db);
2131
- await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`).execute(db);
2533
+ )`).execute(ddlDb);
2534
+ await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`).execute(ddlDb);
2535
+ await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`).execute(ddlDb);
2132
2536
  for (const [colName, col] of Object.entries(tableDef.columns)) {
2133
2537
  if (col.indexed) {
2134
- await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`).execute(db);
2538
+ await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`).execute(ddlDb);
2135
2539
  }
2136
2540
  if (col.search) {
2137
- await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`).execute(db);
2541
+ await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`).execute(ddlDb);
2138
2542
  }
2139
2543
  }
2140
2544
  }
@@ -2153,12 +2557,12 @@ async function deploySchema(db, def, handlerPath, opts) {
2153
2557
  if (!sqlType)
2154
2558
  continue;
2155
2559
  const nullable = col.nullable ? "" : ` NOT NULL DEFAULT ${getDefault(col.type)}`;
2156
- await sql4.raw(`ALTER TABLE ${qualifiedName} ADD COLUMN ${colName} ${sqlType}${nullable}`).execute(db);
2560
+ await sql4.raw(`ALTER TABLE ${qualifiedName} ADD COLUMN ${colName} ${sqlType}${nullable}`).execute(ddlDb);
2157
2561
  if (col.indexed) {
2158
- await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`).execute(db);
2562
+ await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`).execute(ddlDb);
2159
2563
  }
2160
2564
  if (col.search) {
2161
- await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`).execute(db);
2565
+ await sql4.raw(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`).execute(ddlDb);
2162
2566
  }
2163
2567
  }
2164
2568
  }
@@ -2183,7 +2587,7 @@ async function deploySchema(db, def, handlerPath, opts) {
2183
2587
  }
2184
2588
  }
2185
2589
  for (const stmt of statements) {
2186
- await sql4.raw(stmt).execute(db);
2590
+ await sql4.raw(stmt).execute(ddlDb);
2187
2591
  }
2188
2592
  const sg = await registerSubgraph(db, regData);
2189
2593
  return { action: "created", subgraphId: sg.id, version: newVersion };
@@ -2209,14 +2613,17 @@ function getDefault(type) {
2209
2613
  export {
2210
2614
  validateSubgraphDefinition,
2211
2615
  resumeReindex,
2616
+ renderDeployPlan,
2212
2617
  reindexSubgraph,
2213
2618
  pgSchemaName,
2214
2619
  generateSubgraphSQL,
2620
+ generatePrismaSchema,
2621
+ generateDrizzleSchema,
2215
2622
  diffSchema,
2216
2623
  deploySchema,
2217
2624
  defineSubgraph,
2218
2625
  backfillSubgraph
2219
2626
  };
2220
2627
 
2221
- //# debugId=E652DFA8BE73568E64756E2164756E21
2628
+ //# debugId=12416214D391B0FF64756E2164756E21
2222
2629
  //# sourceMappingURL=index.js.map