@secondlayer/subgraphs 4.4.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -21
- package/dist/{src/index.d.ts → index.d.ts} +59 -189
- package/dist/index.js +2151 -0
- package/dist/index.js.map +24 -0
- package/dist/{src/runtime → runtime}/emitter.d.ts +1 -1
- package/dist/{src/runtime → runtime}/emitter.js +8 -12
- package/dist/{src/runtime → runtime}/emitter.js.map +3 -3
- package/dist/{src/runtime → runtime}/replay.d.ts +1 -1
- package/dist/{src/runtime → runtime}/replay.js +342 -683
- package/dist/{src/runtime → runtime}/replay.js.map +5 -5
- package/dist/{src/schema → schema}/index.d.ts +10 -69
- package/dist/{src/schema → schema}/index.js +188 -281
- package/dist/schema/index.js.map +13 -0
- package/dist/{src/testing → testing}/index.d.ts +65 -115
- package/dist/{src/testing → testing}/index.js +554 -968
- package/dist/testing/index.js.map +21 -0
- package/dist/{src/types.d.ts → types.d.ts} +10 -66
- package/dist/{src/validate.d.ts → validate.d.ts} +11 -69
- package/dist/{src/validate.js → validate.js} +18 -98
- package/dist/validate.js.map +10 -0
- package/package.json +18 -26
- package/dist/src/index.js +0 -6081
- package/dist/src/index.js.map +0 -44
- package/dist/src/runtime/block-processor.d.ts +0 -559
- package/dist/src/runtime/block-processor.js +0 -3810
- package/dist/src/runtime/block-processor.js.map +0 -31
- package/dist/src/runtime/catchup.d.ts +0 -473
- package/dist/src/runtime/catchup.js +0 -4228
- package/dist/src/runtime/catchup.js.map +0 -35
- package/dist/src/runtime/clarity.d.ts +0 -15
- package/dist/src/runtime/clarity.js +0 -41
- package/dist/src/runtime/clarity.js.map +0 -10
- package/dist/src/runtime/context.d.ts +0 -325
- package/dist/src/runtime/context.js +0 -936
- package/dist/src/runtime/context.js.map +0 -13
- package/dist/src/runtime/processor.d.ts +0 -11
- package/dist/src/runtime/processor.js +0 -5372
- package/dist/src/runtime/processor.js.map +0 -40
- package/dist/src/runtime/reindex.d.ts +0 -539
- package/dist/src/runtime/reindex.js +0 -4802
- package/dist/src/runtime/reindex.js.map +0 -37
- package/dist/src/runtime/reorg.d.ts +0 -432
- package/dist/src/runtime/reorg.js +0 -4355
- package/dist/src/runtime/reorg.js.map +0 -36
- package/dist/src/runtime/runner.d.ts +0 -719
- package/dist/src/runtime/runner.js +0 -1332
- package/dist/src/runtime/runner.js.map +0 -16
- package/dist/src/runtime/source-matcher.d.ts +0 -304
- package/dist/src/runtime/source-matcher.js +0 -335
- package/dist/src/runtime/source-matcher.js.map +0 -10
- package/dist/src/runtime/stats.d.ts +0 -23
- package/dist/src/runtime/stats.js +0 -71
- package/dist/src/runtime/stats.js.map +0 -10
- package/dist/src/schema/index.js.map +0 -13
- package/dist/src/service.d.ts +0 -0
- package/dist/src/service.js +0 -5398
- package/dist/src/service.js.map +0 -41
- package/dist/src/testing/index.js.map +0 -22
- package/dist/src/triggers/index.d.ts +0 -381
- package/dist/src/triggers/index.js +0 -31
- package/dist/src/triggers/index.js.map +0 -10
- package/dist/src/validate.js.map +0 -10
- /package/dist/{src/types.js → types.js} +0 -0
- /package/dist/{src/types.js.map → types.js.map} +0 -0
|
@@ -1,5 +1,171 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// src/schema/generator.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
// src/schema/utils.ts
|
|
5
|
+
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
6
|
+
function quotePgIdent(name) {
|
|
7
|
+
if (/^[a-z_][a-z0-9_]*$/i.test(name))
|
|
8
|
+
return name;
|
|
9
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
10
|
+
}
|
|
11
|
+
function snakeToCamel(name) {
|
|
12
|
+
return name.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
13
|
+
}
|
|
14
|
+
function pascalCase(name) {
|
|
15
|
+
const camel = snakeToCamel(name);
|
|
16
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/schema/generator.ts
|
|
20
|
+
var TYPE_MAP = {
|
|
21
|
+
text: "TEXT",
|
|
22
|
+
uint: "NUMERIC",
|
|
23
|
+
int: "NUMERIC",
|
|
24
|
+
principal: "TEXT",
|
|
25
|
+
boolean: "BOOLEAN",
|
|
26
|
+
timestamp: "TIMESTAMPTZ",
|
|
27
|
+
jsonb: "JSONB"
|
|
28
|
+
};
|
|
29
|
+
var MAX_IDENT_BYTES = 63;
|
|
30
|
+
function safeIndexName(name) {
|
|
31
|
+
if (name.length <= MAX_IDENT_BYTES)
|
|
32
|
+
return name;
|
|
33
|
+
const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
|
|
34
|
+
return `${name.slice(0, MAX_IDENT_BYTES - hash.length - 1)}_${hash}`;
|
|
35
|
+
}
|
|
36
|
+
function emitIndexedColumnIndexDDL(schemaName, tableName, colName, opts) {
|
|
37
|
+
const concurrently = opts?.concurrently ? " CONCURRENTLY" : "";
|
|
38
|
+
const qualified = `${quotePgIdent(schemaName)}.${quotePgIdent(tableName)}`;
|
|
39
|
+
const plain = `idx_${schemaName}_${tableName}_${colName}`;
|
|
40
|
+
const composite = safeIndexName(`idx_${schemaName}_${tableName}_${colName}_id`);
|
|
41
|
+
const col = quotePgIdent(colName);
|
|
42
|
+
const id = quotePgIdent("_id");
|
|
43
|
+
return {
|
|
44
|
+
create: [
|
|
45
|
+
`CREATE INDEX${concurrently} IF NOT EXISTS ${quotePgIdent(plain)} ON ${qualified} (${col})`,
|
|
46
|
+
`CREATE INDEX${concurrently} IF NOT EXISTS ${quotePgIdent(composite)} ON ${qualified} (${col}, ${id})`
|
|
47
|
+
],
|
|
48
|
+
drop: [
|
|
49
|
+
`DROP INDEX${concurrently} IF EXISTS ${quotePgIdent(schemaName)}.${quotePgIdent(plain)}`,
|
|
50
|
+
`DROP INDEX${concurrently} IF EXISTS ${quotePgIdent(schemaName)}.${quotePgIdent(composite)}`
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function escapeLiteralDefault(value) {
|
|
55
|
+
if (value === null || value === undefined)
|
|
56
|
+
return "NULL";
|
|
57
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
58
|
+
return String(value);
|
|
59
|
+
if (typeof value === "boolean")
|
|
60
|
+
return value ? "TRUE" : "FALSE";
|
|
61
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
62
|
+
}
|
|
63
|
+
function tableNeedsTrgm(tableDef) {
|
|
64
|
+
return Object.values(tableDef.columns).some((col) => col.search);
|
|
65
|
+
}
|
|
66
|
+
function emitTableDDL(schemaName, tableName, tableDef) {
|
|
67
|
+
const qualifiedName = `${quotePgIdent(schemaName)}.${quotePgIdent(tableName)}`;
|
|
68
|
+
const statements = [];
|
|
69
|
+
const columnDefs = [
|
|
70
|
+
"_id BIGSERIAL PRIMARY KEY",
|
|
71
|
+
"_block_height BIGINT NOT NULL",
|
|
72
|
+
"_tx_id TEXT NOT NULL",
|
|
73
|
+
"_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
|
|
74
|
+
];
|
|
75
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
76
|
+
const sqlType = TYPE_MAP[col.type];
|
|
77
|
+
const nullable = col.nullable ? "" : " NOT NULL";
|
|
78
|
+
let colDef = `${quotePgIdent(colName)} ${sqlType}${nullable}`;
|
|
79
|
+
if (col.default !== undefined) {
|
|
80
|
+
colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
|
|
81
|
+
}
|
|
82
|
+
if (col.type === "uint") {
|
|
83
|
+
colDef += ` CHECK (${quotePgIdent(colName)} >= 0)`;
|
|
84
|
+
}
|
|
85
|
+
columnDefs.push(colDef);
|
|
86
|
+
}
|
|
87
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
|
|
88
|
+
${columnDefs.join(`,
|
|
89
|
+
`)}
|
|
90
|
+
)`);
|
|
91
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_block_height`)} ON ${qualifiedName} (_block_height)`);
|
|
92
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_tx_id`)} ON ${qualifiedName} (_tx_id)`);
|
|
93
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
94
|
+
if (col.indexed) {
|
|
95
|
+
statements.push(...emitIndexedColumnIndexDDL(schemaName, tableName, colName).create);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
99
|
+
if (col.search) {
|
|
100
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_${colName}_trgm`)} ON ${qualifiedName} USING gin (${quotePgIdent(colName)} gin_trgm_ops)`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (tableDef.indexes) {
|
|
104
|
+
for (let i = 0;i < tableDef.indexes.length; i++) {
|
|
105
|
+
const cols = tableDef.indexes[i];
|
|
106
|
+
const idxName = quotePgIdent(`idx_${schemaName}_${tableName}_composite_${i}`);
|
|
107
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.map(quotePgIdent).join(", ")})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (tableDef.uniqueKeys) {
|
|
111
|
+
for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
|
|
112
|
+
const cols = tableDef.uniqueKeys[i];
|
|
113
|
+
const constraintName = quotePgIdent(`uq_${schemaName}_${tableName}_${cols.join("_")}`);
|
|
114
|
+
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.map(quotePgIdent).join(", ")})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return statements;
|
|
118
|
+
}
|
|
119
|
+
function emitJournalDDL(schemaName) {
|
|
120
|
+
return [
|
|
121
|
+
`CREATE TABLE IF NOT EXISTS ${schemaName}._journal (
|
|
122
|
+
_jid BIGSERIAL PRIMARY KEY,
|
|
123
|
+
block_height BIGINT NOT NULL,
|
|
124
|
+
table_name TEXT NOT NULL,
|
|
125
|
+
row_key JSONB NOT NULL,
|
|
126
|
+
prev_row JSONB,
|
|
127
|
+
_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
128
|
+
)`,
|
|
129
|
+
`CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
function emitFactoryDDL(schemaName) {
|
|
133
|
+
return [
|
|
134
|
+
`CREATE TABLE IF NOT EXISTS ${schemaName}._factory_addresses (
|
|
135
|
+
source_name TEXT NOT NULL,
|
|
136
|
+
address TEXT NOT NULL,
|
|
137
|
+
block_height BIGINT NOT NULL,
|
|
138
|
+
_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
139
|
+
PRIMARY KEY (source_name, address)
|
|
140
|
+
)`,
|
|
141
|
+
`CREATE INDEX IF NOT EXISTS idx_${schemaName}_factory_height ON ${schemaName}._factory_addresses (block_height)`
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
function generateSubgraphSQL(def, schemaNameOverride) {
|
|
145
|
+
const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
|
|
146
|
+
const statements = [];
|
|
147
|
+
const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
|
|
148
|
+
if (needsTrgm) {
|
|
149
|
+
statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
|
|
150
|
+
}
|
|
151
|
+
statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
|
|
152
|
+
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
153
|
+
statements.push(...emitTableDDL(schemaName, tableName, tableDef));
|
|
154
|
+
}
|
|
155
|
+
statements.push(...emitJournalDDL(schemaName));
|
|
156
|
+
if (Object.values(def.sources ?? {}).some((src) => src.factory !== undefined)) {
|
|
157
|
+
statements.push(...emitFactoryDDL(schemaName));
|
|
158
|
+
}
|
|
159
|
+
const hashInput = JSON.stringify({
|
|
160
|
+
name: def.name,
|
|
161
|
+
schema: def.schema,
|
|
162
|
+
sources: def.sources
|
|
163
|
+
}, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
164
|
+
const hash = createHash("sha256").update(hashInput).digest("hex");
|
|
165
|
+
return { statements, hash };
|
|
166
|
+
}
|
|
167
|
+
// src/schema/deployer.ts
|
|
168
|
+
import { sql } from "kysely";
|
|
3
169
|
|
|
4
170
|
// src/validate.ts
|
|
5
171
|
import { z } from "zod";
|
|
@@ -62,13 +228,7 @@ var SubgraphColumnSchema = z.object({
|
|
|
62
228
|
var SubgraphTableSchema = z.object({
|
|
63
229
|
columns: z.record(SqlIdentifierSchema, SubgraphColumnSchema).refine((c) => Object.keys(c).length > 0, "Table must have at least one column"),
|
|
64
230
|
indexes: z.array(z.array(SqlIdentifierSchema)).optional(),
|
|
65
|
-
uniqueKeys: z.array(z.array(SqlIdentifierSchema)).optional()
|
|
66
|
-
relations: z.array(z.object({
|
|
67
|
-
name: SqlIdentifierSchema,
|
|
68
|
-
references: SqlIdentifierSchema,
|
|
69
|
-
fields: z.array(SqlIdentifierSchema).min(1),
|
|
70
|
-
referencedColumns: z.array(SqlIdentifierSchema).min(1)
|
|
71
|
-
})).optional()
|
|
231
|
+
uniqueKeys: z.array(z.array(SqlIdentifierSchema)).optional()
|
|
72
232
|
});
|
|
73
233
|
var SubgraphSchemaSchema = z.record(SqlIdentifierSchema, SubgraphTableSchema).refine((s) => Object.keys(s).length > 0, "Schema must have at least one table");
|
|
74
234
|
var VALID_FILTER_TYPES = [
|
|
@@ -103,15 +263,6 @@ var amountRange = {
|
|
|
103
263
|
minAmount: z.bigint().optional(),
|
|
104
264
|
maxAmount: z.bigint().optional()
|
|
105
265
|
};
|
|
106
|
-
var MaterializeColumnSchema = z.union([
|
|
107
|
-
z.object({ from: z.string().min(1) }).strict(),
|
|
108
|
-
z.object({ fromTx: z.enum(["sender", "txId"]) }).strict(),
|
|
109
|
-
z.object({ fromBlock: z.enum(["height", "hash", "timestamp"]) }).strict()
|
|
110
|
-
]);
|
|
111
|
-
var MaterializeSpecSchema = z.object({
|
|
112
|
-
table: z.string().min(1),
|
|
113
|
-
columns: z.record(z.string(), MaterializeColumnSchema)
|
|
114
|
-
}).strict();
|
|
115
266
|
var SubgraphFilterUnion = z.discriminatedUnion("type", [
|
|
116
267
|
z.object({
|
|
117
268
|
type: z.literal("stx_transfer"),
|
|
@@ -194,7 +345,6 @@ var SubgraphFilterUnion = z.discriminatedUnion("type", [
|
|
|
194
345
|
contractId: contractIdField.optional(),
|
|
195
346
|
topic: z.string().optional(),
|
|
196
347
|
prints: z.record(z.string(), z.record(z.string(), PrintFieldSchema)).optional(),
|
|
197
|
-
materialize: MaterializeSpecSchema.optional(),
|
|
198
348
|
...traitScope,
|
|
199
349
|
...factoryScope
|
|
200
350
|
}).strict(),
|
|
@@ -244,54 +394,6 @@ function hasPinnedContractId(contractId) {
|
|
|
244
394
|
return contractId.length > 0;
|
|
245
395
|
return false;
|
|
246
396
|
}
|
|
247
|
-
function printMaterializeFromKeys(prints) {
|
|
248
|
-
const keys = new Set(["topic"]);
|
|
249
|
-
if (prints) {
|
|
250
|
-
for (const fields of Object.values(prints)) {
|
|
251
|
-
for (const k of Object.keys(fields))
|
|
252
|
-
keys.add(k);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return [...keys].sort();
|
|
256
|
-
}
|
|
257
|
-
function refineMaterialize(name, filter, schema, ctx) {
|
|
258
|
-
if (!("materialize" in filter) || filter.materialize === undefined)
|
|
259
|
-
return;
|
|
260
|
-
const spec = filter.materialize;
|
|
261
|
-
const table = schema[spec.table];
|
|
262
|
-
if (!table) {
|
|
263
|
-
ctx.addIssue({
|
|
264
|
-
code: "custom",
|
|
265
|
-
path: ["sources", name, "materialize", "table"],
|
|
266
|
-
message: `materialize table "${spec.table}" is not in schema`
|
|
267
|
-
});
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
for (const col of Object.keys(spec.columns)) {
|
|
271
|
-
if (!(col in table.columns)) {
|
|
272
|
-
ctx.addIssue({
|
|
273
|
-
code: "custom",
|
|
274
|
-
path: ["sources", name, "materialize", "columns", col],
|
|
275
|
-
message: `materialize column "${col}" is not on schema table "${spec.table}"`
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (filter.type === "print_event") {
|
|
280
|
-
const known = printMaterializeFromKeys(filter.prints);
|
|
281
|
-
const knownSet = new Set(known);
|
|
282
|
-
for (const [col, mapping] of Object.entries(spec.columns)) {
|
|
283
|
-
if (!("from" in mapping))
|
|
284
|
-
continue;
|
|
285
|
-
if (!knownSet.has(mapping.from)) {
|
|
286
|
-
ctx.addIssue({
|
|
287
|
-
code: "custom",
|
|
288
|
-
path: ["sources", name, "materialize", "columns", col, "from"],
|
|
289
|
-
message: `materialize from "${mapping.from}" is not a known print field — known: ${known.join(", ")}`
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
397
|
function refineDeployGates(def, ctx) {
|
|
296
398
|
for (const [name, filter] of Object.entries(def.sources)) {
|
|
297
399
|
if (filter.type === "print_event") {
|
|
@@ -312,36 +414,23 @@ function refineDeployGates(def, ctx) {
|
|
|
312
414
|
ctx.addIssue({
|
|
313
415
|
code: "custom",
|
|
314
416
|
path: ["sources", name, "abi"],
|
|
315
|
-
message: `contract_call source "${name}" has functionName but no abi
|
|
417
|
+
message: `contract_call source "${name}" has functionName but no abi. \`secondlayer subgraphs deploy\` fills it in from the deployed contract; otherwise pass abi (normalizeAbi() from @secondlayer/stacks/clarity).`
|
|
316
418
|
});
|
|
317
419
|
}
|
|
318
420
|
}
|
|
319
|
-
refineMaterialize(name, filter, def.schema, ctx);
|
|
320
421
|
}
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
const hasMaterialize = "materialize" in filter && filter.materialize !== undefined;
|
|
324
|
-
const hasHandler = def.handlers[name] != null;
|
|
325
|
-
if (hasMaterialize && hasHandler) {
|
|
326
|
-
ctx.addIssue({
|
|
327
|
-
code: "custom",
|
|
328
|
-
path: ["handlers", name],
|
|
329
|
-
message: `source "${name}" has both materialize and a handler — use one or the other`
|
|
330
|
-
});
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (!hasHandler && !hasMaterialize && !hasCatchAll) {
|
|
422
|
+
for (const name of Object.keys(def.sources)) {
|
|
423
|
+
if (def.handlers[name] == null) {
|
|
334
424
|
ctx.addIssue({
|
|
335
425
|
code: "custom",
|
|
336
426
|
path: ["handlers", name],
|
|
337
|
-
message: `source "${name}" has no handler
|
|
427
|
+
message: `source "${name}" has no handler. Add handlers.${name}, or its events are skipped and the table stays empty.`
|
|
338
428
|
});
|
|
339
429
|
}
|
|
340
430
|
}
|
|
341
431
|
}
|
|
342
432
|
var SubgraphDefinitionSchema = z.object({
|
|
343
433
|
name: SubgraphNameSchema,
|
|
344
|
-
version: z.string().optional(),
|
|
345
434
|
description: z.string().optional(),
|
|
346
435
|
startBlock: z.number().int().nonnegative().optional(),
|
|
347
436
|
backfillMode: z.enum(["blocking", "concurrent"]).optional(),
|
|
@@ -372,183 +461,7 @@ function filterFieldsByType() {
|
|
|
372
461
|
return out;
|
|
373
462
|
}
|
|
374
463
|
|
|
375
|
-
// src/schema/generator.ts
|
|
376
|
-
import { createHash } from "node:crypto";
|
|
377
|
-
|
|
378
|
-
// src/schema/utils.ts
|
|
379
|
-
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
380
|
-
function quotePgIdent(name) {
|
|
381
|
-
if (/^[a-z_][a-z0-9_]*$/i.test(name))
|
|
382
|
-
return name;
|
|
383
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
384
|
-
}
|
|
385
|
-
function snakeToCamel(name) {
|
|
386
|
-
return name.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
387
|
-
}
|
|
388
|
-
function pascalCase(name) {
|
|
389
|
-
const camel = snakeToCamel(name);
|
|
390
|
-
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// src/schema/generator.ts
|
|
394
|
-
var TYPE_MAP = {
|
|
395
|
-
text: "TEXT",
|
|
396
|
-
uint: "NUMERIC",
|
|
397
|
-
int: "NUMERIC",
|
|
398
|
-
principal: "TEXT",
|
|
399
|
-
boolean: "BOOLEAN",
|
|
400
|
-
timestamp: "TIMESTAMPTZ",
|
|
401
|
-
jsonb: "JSONB"
|
|
402
|
-
};
|
|
403
|
-
var MAX_IDENT_BYTES = 63;
|
|
404
|
-
function safeIndexName(name) {
|
|
405
|
-
if (name.length <= MAX_IDENT_BYTES)
|
|
406
|
-
return name;
|
|
407
|
-
const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
|
|
408
|
-
return `${name.slice(0, MAX_IDENT_BYTES - hash.length - 1)}_${hash}`;
|
|
409
|
-
}
|
|
410
|
-
function emitIndexedColumnIndexDDL(schemaName, tableName, colName, opts) {
|
|
411
|
-
const concurrently = opts?.concurrently ? " CONCURRENTLY" : "";
|
|
412
|
-
const qualified = `${quotePgIdent(schemaName)}.${quotePgIdent(tableName)}`;
|
|
413
|
-
const plain = `idx_${schemaName}_${tableName}_${colName}`;
|
|
414
|
-
const composite = safeIndexName(`idx_${schemaName}_${tableName}_${colName}_id`);
|
|
415
|
-
const col = quotePgIdent(colName);
|
|
416
|
-
const id = quotePgIdent("_id");
|
|
417
|
-
return {
|
|
418
|
-
create: [
|
|
419
|
-
`CREATE INDEX${concurrently} IF NOT EXISTS ${quotePgIdent(plain)} ON ${qualified} (${col})`,
|
|
420
|
-
`CREATE INDEX${concurrently} IF NOT EXISTS ${quotePgIdent(composite)} ON ${qualified} (${col}, ${id})`
|
|
421
|
-
],
|
|
422
|
-
drop: [
|
|
423
|
-
`DROP INDEX${concurrently} IF EXISTS ${quotePgIdent(schemaName)}.${quotePgIdent(plain)}`,
|
|
424
|
-
`DROP INDEX${concurrently} IF EXISTS ${quotePgIdent(schemaName)}.${quotePgIdent(composite)}`
|
|
425
|
-
]
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
function escapeLiteralDefault(value) {
|
|
429
|
-
if (value === null || value === undefined)
|
|
430
|
-
return "NULL";
|
|
431
|
-
if (typeof value === "number" || typeof value === "bigint")
|
|
432
|
-
return String(value);
|
|
433
|
-
if (typeof value === "boolean")
|
|
434
|
-
return value ? "TRUE" : "FALSE";
|
|
435
|
-
return `'${String(value).replace(/'/g, "''")}'`;
|
|
436
|
-
}
|
|
437
|
-
function tableNeedsTrgm(tableDef) {
|
|
438
|
-
return Object.values(tableDef.columns).some((col) => col.search);
|
|
439
|
-
}
|
|
440
|
-
function emitTableDDL(schemaName, tableName, tableDef) {
|
|
441
|
-
const qualifiedName = `${quotePgIdent(schemaName)}.${quotePgIdent(tableName)}`;
|
|
442
|
-
const statements = [];
|
|
443
|
-
const columnDefs = [
|
|
444
|
-
"_id BIGSERIAL PRIMARY KEY",
|
|
445
|
-
"_block_height BIGINT NOT NULL",
|
|
446
|
-
"_tx_id TEXT NOT NULL",
|
|
447
|
-
"_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
|
|
448
|
-
];
|
|
449
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
450
|
-
const sqlType = TYPE_MAP[col.type];
|
|
451
|
-
const nullable = col.nullable ? "" : " NOT NULL";
|
|
452
|
-
let colDef = `${quotePgIdent(colName)} ${sqlType}${nullable}`;
|
|
453
|
-
if (col.default !== undefined) {
|
|
454
|
-
colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
|
|
455
|
-
}
|
|
456
|
-
if (col.type === "uint") {
|
|
457
|
-
colDef += ` CHECK (${quotePgIdent(colName)} >= 0)`;
|
|
458
|
-
}
|
|
459
|
-
columnDefs.push(colDef);
|
|
460
|
-
}
|
|
461
|
-
statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
|
|
462
|
-
${columnDefs.join(`,
|
|
463
|
-
`)}
|
|
464
|
-
)`);
|
|
465
|
-
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_block_height`)} ON ${qualifiedName} (_block_height)`);
|
|
466
|
-
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_tx_id`)} ON ${qualifiedName} (_tx_id)`);
|
|
467
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
468
|
-
if (col.indexed) {
|
|
469
|
-
statements.push(...emitIndexedColumnIndexDDL(schemaName, tableName, colName).create);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
473
|
-
if (col.search) {
|
|
474
|
-
statements.push(`CREATE INDEX IF NOT EXISTS ${quotePgIdent(`idx_${schemaName}_${tableName}_${colName}_trgm`)} ON ${qualifiedName} USING gin (${quotePgIdent(colName)} gin_trgm_ops)`);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
if (tableDef.indexes) {
|
|
478
|
-
for (let i = 0;i < tableDef.indexes.length; i++) {
|
|
479
|
-
const cols = tableDef.indexes[i];
|
|
480
|
-
const idxName = quotePgIdent(`idx_${schemaName}_${tableName}_composite_${i}`);
|
|
481
|
-
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.map(quotePgIdent).join(", ")})`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
if (tableDef.uniqueKeys) {
|
|
485
|
-
for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
|
|
486
|
-
const cols = tableDef.uniqueKeys[i];
|
|
487
|
-
const constraintName = quotePgIdent(`uq_${schemaName}_${tableName}_${cols.join("_")}`);
|
|
488
|
-
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.map(quotePgIdent).join(", ")})`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
return statements;
|
|
492
|
-
}
|
|
493
|
-
function emitJournalDDL(schemaName) {
|
|
494
|
-
return [
|
|
495
|
-
`CREATE TABLE IF NOT EXISTS ${schemaName}._journal (
|
|
496
|
-
_jid BIGSERIAL PRIMARY KEY,
|
|
497
|
-
block_height BIGINT NOT NULL,
|
|
498
|
-
table_name TEXT NOT NULL,
|
|
499
|
-
row_key JSONB NOT NULL,
|
|
500
|
-
prev_row JSONB,
|
|
501
|
-
_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
502
|
-
)`,
|
|
503
|
-
`CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
|
|
504
|
-
];
|
|
505
|
-
}
|
|
506
|
-
function emitFactoryDDL(schemaName) {
|
|
507
|
-
return [
|
|
508
|
-
`CREATE TABLE IF NOT EXISTS ${schemaName}._factory_addresses (
|
|
509
|
-
source_name TEXT NOT NULL,
|
|
510
|
-
address TEXT NOT NULL,
|
|
511
|
-
block_height BIGINT NOT NULL,
|
|
512
|
-
_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
513
|
-
PRIMARY KEY (source_name, address)
|
|
514
|
-
)`,
|
|
515
|
-
`CREATE INDEX IF NOT EXISTS idx_${schemaName}_factory_height ON ${schemaName}._factory_addresses (block_height)`
|
|
516
|
-
];
|
|
517
|
-
}
|
|
518
|
-
function emitForeignKeyDDL(schemaName, tableName, tableDef) {
|
|
519
|
-
return (tableDef.relations ?? []).map((rel) => {
|
|
520
|
-
const constraintName = quotePgIdent(`fk_${schemaName}_${tableName}_${rel.name}`);
|
|
521
|
-
return `ALTER TABLE ${quotePgIdent(schemaName)}.${quotePgIdent(tableName)} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.map(quotePgIdent).join(", ")}) ` + `REFERENCES ${quotePgIdent(schemaName)}.${quotePgIdent(rel.references)} (${rel.referencedColumns.map(quotePgIdent).join(", ")})`;
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
function generateSubgraphSQL(def, schemaNameOverride) {
|
|
525
|
-
const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
|
|
526
|
-
const statements = [];
|
|
527
|
-
const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
|
|
528
|
-
if (needsTrgm) {
|
|
529
|
-
statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
|
|
530
|
-
}
|
|
531
|
-
statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
|
|
532
|
-
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
533
|
-
statements.push(...emitTableDDL(schemaName, tableName, tableDef));
|
|
534
|
-
}
|
|
535
|
-
statements.push(...emitJournalDDL(schemaName));
|
|
536
|
-
if (Object.values(def.sources ?? {}).some((src) => src.factory !== undefined)) {
|
|
537
|
-
statements.push(...emitFactoryDDL(schemaName));
|
|
538
|
-
}
|
|
539
|
-
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
540
|
-
statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
|
|
541
|
-
}
|
|
542
|
-
const hashInput = JSON.stringify({
|
|
543
|
-
name: def.name,
|
|
544
|
-
schema: def.schema,
|
|
545
|
-
sources: def.sources
|
|
546
|
-
}, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
547
|
-
const hash = createHash("sha256").update(hashInput).digest("hex");
|
|
548
|
-
return { statements, hash };
|
|
549
|
-
}
|
|
550
464
|
// src/schema/deployer.ts
|
|
551
|
-
import { sql } from "kysely";
|
|
552
465
|
function toJsonSafe(obj) {
|
|
553
466
|
return JSON.parse(JSON.stringify(obj, (_key, value) => typeof value === "bigint" ? value.toString() : value));
|
|
554
467
|
}
|
|
@@ -649,13 +562,12 @@ async function deploySchema(db, def, handlerPath, opts) {
|
|
|
649
562
|
const ddlDb = db;
|
|
650
563
|
const existing = await getSubgraph(db, def.name, opts?.accountId);
|
|
651
564
|
const schemaName = opts?.schemaName ?? pgSchemaName(def.name);
|
|
652
|
-
const newVersion =
|
|
565
|
+
const newVersion = existing ? bumpPatch(existing.version) : "1.0.0";
|
|
653
566
|
const regData = {
|
|
654
567
|
name: def.name,
|
|
655
568
|
version: newVersion,
|
|
656
569
|
definition: toJsonSafe({
|
|
657
570
|
name: def.name,
|
|
658
|
-
version: def.version,
|
|
659
571
|
description: def.description,
|
|
660
572
|
startBlock: def.startBlock,
|
|
661
573
|
sources: def.sources,
|
|
@@ -681,8 +593,8 @@ async function deploySchema(db, def, handlerPath, opts) {
|
|
|
681
593
|
for (const stmt of statements) {
|
|
682
594
|
await sql.raw(stmt).execute(ddlDb);
|
|
683
595
|
}
|
|
684
|
-
const
|
|
685
|
-
return { action: "reindexed", subgraphId:
|
|
596
|
+
const sg = await registerSubgraph(db, regData);
|
|
597
|
+
return { action: "reindexed", subgraphId: sg.id, version: newVersion };
|
|
686
598
|
}
|
|
687
599
|
if (existing.schema_hash === hash && !opts?.forceReindex) {
|
|
688
600
|
const handlerChanged = opts?.handlerCode != null && opts.handlerCode !== existing.handler_code;
|
|
@@ -707,8 +619,8 @@ async function deploySchema(db, def, handlerPath, opts) {
|
|
|
707
619
|
for (const stmt of statements) {
|
|
708
620
|
await sql.raw(stmt).execute(ddlDb);
|
|
709
621
|
}
|
|
710
|
-
const
|
|
711
|
-
return { action: "reindexed", subgraphId:
|
|
622
|
+
const sg = await registerSubgraph(db, regData);
|
|
623
|
+
return { action: "reindexed", subgraphId: sg.id, version: newVersion };
|
|
712
624
|
}
|
|
713
625
|
if (existing.definition.schema) {
|
|
714
626
|
const diff = diffSchema(existing.definition.schema, def.schema);
|
|
@@ -718,13 +630,13 @@ async function deploySchema(db, def, handlerPath, opts) {
|
|
|
718
630
|
for (const stmt of statements) {
|
|
719
631
|
await sql.raw(stmt).execute(ddlDb);
|
|
720
632
|
}
|
|
721
|
-
const
|
|
722
|
-
const
|
|
633
|
+
const sg = await registerSubgraph(db, regData);
|
|
634
|
+
const deployDiff = toDeployDiff(diff, reasons);
|
|
723
635
|
return {
|
|
724
636
|
action: "reindexed",
|
|
725
|
-
subgraphId:
|
|
637
|
+
subgraphId: sg.id,
|
|
726
638
|
version: newVersion,
|
|
727
|
-
diff:
|
|
639
|
+
diff: deployDiff
|
|
728
640
|
};
|
|
729
641
|
}
|
|
730
642
|
const addedDefs = diff.addedTables.map((tableName) => ({ tableName, tableDef: def.schema[tableName] })).filter((t) => t.tableDef !== undefined);
|
|
@@ -736,11 +648,6 @@ async function deploySchema(db, def, handlerPath, opts) {
|
|
|
736
648
|
await sql.raw(stmt).execute(ddlDb);
|
|
737
649
|
}
|
|
738
650
|
}
|
|
739
|
-
for (const { tableName, tableDef } of addedDefs) {
|
|
740
|
-
for (const stmt of emitForeignKeyDDL(schemaName, tableName, tableDef)) {
|
|
741
|
-
await sql.raw(stmt).execute(ddlDb);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
651
|
for (const [tableName, colDiff] of Object.entries(diff.tables)) {
|
|
745
652
|
if (colDiff.added.length === 0)
|
|
746
653
|
continue;
|
|
@@ -830,7 +737,7 @@ async function deploySchema(db, def, handlerPath, opts) {
|
|
|
830
737
|
await sql.raw(`DROP INDEX CONCURRENTLY IF EXISTS ${schemaName}.${idxName}`).execute(ddlDb);
|
|
831
738
|
}
|
|
832
739
|
}
|
|
833
|
-
const
|
|
740
|
+
const sg = await registerSubgraph(db, regData);
|
|
834
741
|
const addedCols = {};
|
|
835
742
|
for (const [t, colDiff] of Object.entries(diff.tables)) {
|
|
836
743
|
if (colDiff.added.length > 0)
|
|
@@ -846,7 +753,7 @@ async function deploySchema(db, def, handlerPath, opts) {
|
|
|
846
753
|
};
|
|
847
754
|
return {
|
|
848
755
|
action: "updated",
|
|
849
|
-
subgraphId:
|
|
756
|
+
subgraphId: sg.id,
|
|
850
757
|
version: newVersion,
|
|
851
758
|
diff: deployDiff
|
|
852
759
|
};
|
|
@@ -877,11 +784,11 @@ function getDefault(type) {
|
|
|
877
784
|
}
|
|
878
785
|
}
|
|
879
786
|
export {
|
|
880
|
-
|
|
881
|
-
diffSchema,
|
|
787
|
+
TYPE_MAP,
|
|
882
788
|
deploySchema,
|
|
883
|
-
|
|
789
|
+
diffSchema,
|
|
790
|
+
generateSubgraphSQL
|
|
884
791
|
};
|
|
885
792
|
|
|
886
|
-
//# debugId=
|
|
793
|
+
//# debugId=054E6A0FD91586DA64756E2164756E21
|
|
887
794
|
//# sourceMappingURL=index.js.map
|