@thru/indexer 0.2.21 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.cjs +6 -308
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -206
- package/dist/index.d.ts +2 -206
- package/dist/index.mjs +16 -309
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -14
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A reusable blockchain indexing framework for building backends that index Thru c
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
pnpm add @thru/indexer @thru/replay @thru/helpers postgres drizzle-orm hono @hono/zod-openapi
|
|
17
|
+
pnpm add @thru/indexer @thru/replay @thru/sdk/helpers postgres drizzle-orm hono @hono/zod-openapi
|
|
18
18
|
pnpm add -D drizzle-kit tsx typescript
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -25,7 +25,7 @@ pnpm add -D drizzle-kit tsx typescript
|
|
|
25
25
|
```typescript
|
|
26
26
|
// src/streams/transfers.ts
|
|
27
27
|
import { create } from "@bufbuild/protobuf";
|
|
28
|
-
import { decodeAddress, encodeAddress, encodeSignature } from "@thru/helpers";
|
|
28
|
+
import { decodeAddress, encodeAddress, encodeSignature } from "@thru/sdk/helpers";
|
|
29
29
|
import { defineEventStream, t } from "@thru/indexer";
|
|
30
30
|
import { FilterSchema, FilterParamValueSchema, type Event } from "@thru/replay";
|
|
31
31
|
import { TokenEvent } from "../abi/token";
|
|
@@ -91,7 +91,7 @@ export default transfers;
|
|
|
91
91
|
|
|
92
92
|
```typescript
|
|
93
93
|
// src/account-streams/token-accounts.ts
|
|
94
|
-
import { decodeAddress, encodeAddress } from "@thru/helpers";
|
|
94
|
+
import { decodeAddress, encodeAddress } from "@thru/sdk/helpers";
|
|
95
95
|
import { defineAccountStream, t } from "@thru/indexer";
|
|
96
96
|
import { TokenAccount } from "../abi/token";
|
|
97
97
|
|
package/dist/index.cjs
CHANGED
|
@@ -4,9 +4,7 @@ var pgCore = require('drizzle-orm/pg-core');
|
|
|
4
4
|
var zod = require('zod');
|
|
5
5
|
var replay = require('@thru/replay');
|
|
6
6
|
var drizzleOrm = require('drizzle-orm');
|
|
7
|
-
var helpers = require('@thru/helpers');
|
|
8
|
-
var zodOpenapi = require('@hono/zod-openapi');
|
|
9
|
-
var drizzleZod = require('drizzle-zod');
|
|
7
|
+
var helpers = require('@thru/sdk/helpers');
|
|
10
8
|
|
|
11
9
|
// src/schema/builder.ts
|
|
12
10
|
function createBuilder(state) {
|
|
@@ -194,7 +192,7 @@ function validateParsedData(schema, data, streamName) {
|
|
|
194
192
|
if (result.success) {
|
|
195
193
|
return { success: true, data: result.data };
|
|
196
194
|
}
|
|
197
|
-
const errorMessages = result.error.
|
|
195
|
+
const errorMessages = result.error.issues.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
198
196
|
return {
|
|
199
197
|
success: false,
|
|
200
198
|
error: `Stream "${streamName}" parse returned invalid data:
|
|
@@ -289,18 +287,18 @@ async function getAllCheckpoints(db) {
|
|
|
289
287
|
// src/checkpoint/index.ts
|
|
290
288
|
function getSchemaExports(config) {
|
|
291
289
|
const { eventStreams = [], accountStreams = [], tableNames = {} } = config;
|
|
292
|
-
const exports
|
|
290
|
+
const exports = {
|
|
293
291
|
checkpointTable
|
|
294
292
|
};
|
|
295
293
|
for (const stream of eventStreams) {
|
|
296
294
|
const exportName = tableNames[stream.name] ?? `${stream.name}Table`;
|
|
297
|
-
exports
|
|
295
|
+
exports[exportName] = stream.table;
|
|
298
296
|
}
|
|
299
297
|
for (const stream of accountStreams) {
|
|
300
298
|
const exportName = tableNames[stream.name] ?? `${stream.name.replace(/-/g, "")}Table`;
|
|
301
|
-
exports
|
|
299
|
+
exports[exportName] = stream.table;
|
|
302
300
|
}
|
|
303
|
-
return exports
|
|
301
|
+
return exports;
|
|
304
302
|
}
|
|
305
303
|
|
|
306
304
|
// src/streams/processor.ts
|
|
@@ -744,297 +742,6 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
|
744
742
|
);
|
|
745
743
|
return stats;
|
|
746
744
|
}
|
|
747
|
-
var paginationQuerySchema = zodOpenapi.z.object({
|
|
748
|
-
limit: zodOpenapi.z.coerce.number().int().min(1).max(100).default(20).openapi({
|
|
749
|
-
description: "Number of results to return (1-100)",
|
|
750
|
-
example: 20
|
|
751
|
-
}),
|
|
752
|
-
offset: zodOpenapi.z.coerce.number().int().min(0).default(0).openapi({
|
|
753
|
-
description: "Number of results to skip",
|
|
754
|
-
example: 0
|
|
755
|
-
}),
|
|
756
|
-
cursor: zodOpenapi.z.string().optional().openapi({
|
|
757
|
-
description: "Cursor for pagination (format: slot:id)",
|
|
758
|
-
example: "3181195:abc123"
|
|
759
|
-
})
|
|
760
|
-
});
|
|
761
|
-
var paginationResponseSchema = zodOpenapi.z.object({
|
|
762
|
-
limit: zodOpenapi.z.number().openapi({ example: 20 }),
|
|
763
|
-
offset: zodOpenapi.z.number().openapi({ example: 0 }),
|
|
764
|
-
hasMore: zodOpenapi.z.boolean().openapi({ example: true }),
|
|
765
|
-
nextCursor: zodOpenapi.z.string().nullable().openapi({ example: "3181195:abc123" })
|
|
766
|
-
});
|
|
767
|
-
function dataResponse(schema) {
|
|
768
|
-
return zodOpenapi.z.object({ data: schema });
|
|
769
|
-
}
|
|
770
|
-
function listResponse(schema) {
|
|
771
|
-
return zodOpenapi.z.object({
|
|
772
|
-
data: zodOpenapi.z.array(schema),
|
|
773
|
-
pagination: paginationResponseSchema
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
var errorSchema = zodOpenapi.z.object({
|
|
777
|
-
error: zodOpenapi.z.string().openapi({ example: "Not found" }),
|
|
778
|
-
code: zodOpenapi.z.string().optional().openapi({ example: "NOT_FOUND" })
|
|
779
|
-
}).openapi("Error");
|
|
780
|
-
function paginate(rows, query, getCursor) {
|
|
781
|
-
const hasMore = rows.length > query.limit;
|
|
782
|
-
const data = hasMore ? rows.slice(0, -1) : rows;
|
|
783
|
-
const lastItem = data[data.length - 1];
|
|
784
|
-
let nextCursor = null;
|
|
785
|
-
if (hasMore && lastItem) {
|
|
786
|
-
if (getCursor) {
|
|
787
|
-
nextCursor = getCursor(lastItem);
|
|
788
|
-
} else {
|
|
789
|
-
const item = lastItem;
|
|
790
|
-
if (item.slot !== void 0 && item.id !== void 0) {
|
|
791
|
-
nextCursor = `${item.slot}:${item.id}`;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return {
|
|
796
|
-
data,
|
|
797
|
-
pagination: {
|
|
798
|
-
limit: query.limit,
|
|
799
|
-
offset: query.offset,
|
|
800
|
-
hasMore,
|
|
801
|
-
nextCursor
|
|
802
|
-
}
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
function parseCursor(cursor) {
|
|
806
|
-
const colonIndex = cursor.indexOf(":");
|
|
807
|
-
if (colonIndex === -1) return null;
|
|
808
|
-
try {
|
|
809
|
-
const slot = BigInt(cursor.slice(0, colonIndex));
|
|
810
|
-
const id = cursor.slice(colonIndex + 1);
|
|
811
|
-
return { slot, id };
|
|
812
|
-
} catch {
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
function pascalCase3(str) {
|
|
817
|
-
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
818
|
-
}
|
|
819
|
-
function generateSchemas(table, name, suffix = "") {
|
|
820
|
-
const rowSchema = drizzleZod.createSelectSchema(table);
|
|
821
|
-
const insertSchema = drizzleZod.createInsertSchema(table);
|
|
822
|
-
const apiFields = {};
|
|
823
|
-
const columns = drizzleOrm.getTableColumns(table);
|
|
824
|
-
for (const [colName, col] of Object.entries(columns)) {
|
|
825
|
-
const dataType = col.dataType;
|
|
826
|
-
const notNull = col.notNull;
|
|
827
|
-
let fieldSchema;
|
|
828
|
-
switch (dataType) {
|
|
829
|
-
case "bigint":
|
|
830
|
-
fieldSchema = zodOpenapi.z.string().openapi({ description: `${colName} (bigint)` });
|
|
831
|
-
break;
|
|
832
|
-
case "date":
|
|
833
|
-
fieldSchema = zodOpenapi.z.string().openapi({ description: `${colName} (ISO timestamp)` });
|
|
834
|
-
break;
|
|
835
|
-
case "string":
|
|
836
|
-
fieldSchema = zodOpenapi.z.string();
|
|
837
|
-
break;
|
|
838
|
-
case "number":
|
|
839
|
-
fieldSchema = zodOpenapi.z.number();
|
|
840
|
-
break;
|
|
841
|
-
case "boolean":
|
|
842
|
-
fieldSchema = zodOpenapi.z.boolean();
|
|
843
|
-
break;
|
|
844
|
-
default:
|
|
845
|
-
fieldSchema = zodOpenapi.z.any();
|
|
846
|
-
}
|
|
847
|
-
if (!notNull) {
|
|
848
|
-
fieldSchema = fieldSchema.nullable();
|
|
849
|
-
}
|
|
850
|
-
apiFields[colName] = fieldSchema;
|
|
851
|
-
}
|
|
852
|
-
const schemaName = suffix ? `${pascalCase3(name)}${suffix}` : pascalCase3(name);
|
|
853
|
-
const apiSchema = zodOpenapi.z.object(apiFields).openapi(schemaName);
|
|
854
|
-
const serialize = (row) => {
|
|
855
|
-
const result = {};
|
|
856
|
-
for (const [key, value] of Object.entries(row)) {
|
|
857
|
-
if (typeof value === "bigint") {
|
|
858
|
-
result[key] = value.toString();
|
|
859
|
-
} else if (value instanceof Date) {
|
|
860
|
-
result[key] = value.toISOString();
|
|
861
|
-
} else {
|
|
862
|
-
result[key] = value;
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
return result;
|
|
866
|
-
};
|
|
867
|
-
return {
|
|
868
|
-
row: rowSchema,
|
|
869
|
-
insert: insertSchema,
|
|
870
|
-
api: apiSchema,
|
|
871
|
-
serialize
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// src/api/routes.ts
|
|
876
|
-
function pascalCase4(str) {
|
|
877
|
-
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
878
|
-
}
|
|
879
|
-
function buildRoutes(config) {
|
|
880
|
-
const {
|
|
881
|
-
db,
|
|
882
|
-
name,
|
|
883
|
-
table,
|
|
884
|
-
schemas,
|
|
885
|
-
filters,
|
|
886
|
-
idField,
|
|
887
|
-
resourceType,
|
|
888
|
-
includeSlotFilters,
|
|
889
|
-
sortField,
|
|
890
|
-
secondarySortField
|
|
891
|
-
} = config;
|
|
892
|
-
const router = new zodOpenapi.OpenAPIHono();
|
|
893
|
-
const tag = pascalCase4(name);
|
|
894
|
-
const filterFields = {};
|
|
895
|
-
for (const field of filters) {
|
|
896
|
-
filterFields[field] = zodOpenapi.z.string().optional().openapi({
|
|
897
|
-
description: `Filter by ${field}`
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
if (includeSlotFilters) {
|
|
901
|
-
filterFields.fromSlot = zodOpenapi.z.string().optional().openapi({
|
|
902
|
-
description: "Minimum slot number"
|
|
903
|
-
});
|
|
904
|
-
filterFields.toSlot = zodOpenapi.z.string().optional().openapi({
|
|
905
|
-
description: "Maximum slot number"
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
const listQuerySchema = paginationQuerySchema.extend(filterFields);
|
|
909
|
-
const listRoute = zodOpenapi.createRoute({
|
|
910
|
-
method: "get",
|
|
911
|
-
path: "/",
|
|
912
|
-
tags: [tag],
|
|
913
|
-
summary: `List ${name} ${resourceType}s`,
|
|
914
|
-
description: `Returns a paginated list of ${name} ${resourceType}s with optional filtering.`,
|
|
915
|
-
request: { query: listQuerySchema },
|
|
916
|
-
responses: {
|
|
917
|
-
200: {
|
|
918
|
-
description: `List of ${name} ${resourceType}s`,
|
|
919
|
-
content: { "application/json": { schema: listResponse(schemas.api) } }
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
});
|
|
923
|
-
router.openapi(listRoute, async (c) => {
|
|
924
|
-
const query = c.req.valid("query");
|
|
925
|
-
const conditions = [];
|
|
926
|
-
for (const field of filters) {
|
|
927
|
-
const value = query[field];
|
|
928
|
-
if (value !== void 0 && value !== "") {
|
|
929
|
-
conditions.push(drizzleOrm.eq(table[field], value));
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
if (includeSlotFilters) {
|
|
933
|
-
if (query.fromSlot) {
|
|
934
|
-
conditions.push(drizzleOrm.gte(table.slot, BigInt(query.fromSlot)));
|
|
935
|
-
}
|
|
936
|
-
if (query.toSlot) {
|
|
937
|
-
conditions.push(drizzleOrm.lte(table.slot, BigInt(query.toSlot)));
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
if (query.cursor && secondarySortField) {
|
|
941
|
-
const parsed = parseCursor(query.cursor);
|
|
942
|
-
if (parsed) {
|
|
943
|
-
conditions.push(
|
|
944
|
-
drizzleOrm.sql`(${table.slot} < ${parsed.slot} OR (${table.slot} = ${parsed.slot} AND ${table[secondarySortField]} < ${parsed.id}))`
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
const orderBy = secondarySortField ? [drizzleOrm.desc(table[sortField]), drizzleOrm.desc(table[secondarySortField])] : [drizzleOrm.desc(table[sortField])];
|
|
949
|
-
const rows = await db.select().from(table).where(conditions.length > 0 ? drizzleOrm.and(...conditions) : void 0).orderBy(...orderBy).limit(query.limit + 1).offset(query.cursor ? 0 : query.offset);
|
|
950
|
-
const result = paginate(rows, query);
|
|
951
|
-
return c.json({
|
|
952
|
-
data: result.data.map(
|
|
953
|
-
(row) => schemas.serialize(row)
|
|
954
|
-
),
|
|
955
|
-
pagination: result.pagination
|
|
956
|
-
});
|
|
957
|
-
});
|
|
958
|
-
const getRoute = zodOpenapi.createRoute({
|
|
959
|
-
method: "get",
|
|
960
|
-
path: `/{${idField}}`,
|
|
961
|
-
tags: [tag],
|
|
962
|
-
summary: `Get ${name} ${resourceType} by ${idField}`,
|
|
963
|
-
description: `Returns a single ${name} ${resourceType} by its ${idField}.`,
|
|
964
|
-
request: {
|
|
965
|
-
params: zodOpenapi.z.object({
|
|
966
|
-
[idField]: zodOpenapi.z.string().openapi({ description: `${pascalCase4(resourceType)} ${idField}` })
|
|
967
|
-
})
|
|
968
|
-
},
|
|
969
|
-
responses: {
|
|
970
|
-
200: {
|
|
971
|
-
description: `${pascalCase4(name)} ${resourceType} found`,
|
|
972
|
-
content: { "application/json": { schema: dataResponse(schemas.api) } }
|
|
973
|
-
},
|
|
974
|
-
404: {
|
|
975
|
-
description: `${pascalCase4(resourceType)} not found`,
|
|
976
|
-
content: { "application/json": { schema: errorSchema } }
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
});
|
|
980
|
-
router.openapi(getRoute, async (c) => {
|
|
981
|
-
const id = c.req.param(idField);
|
|
982
|
-
const [row] = await db.select().from(table).where(drizzleOrm.eq(table[idField], id)).limit(1);
|
|
983
|
-
if (!row) {
|
|
984
|
-
return c.json({ error: "Not found" }, 404);
|
|
985
|
-
}
|
|
986
|
-
return c.json({
|
|
987
|
-
data: schemas.serialize(row)
|
|
988
|
-
});
|
|
989
|
-
});
|
|
990
|
-
return router;
|
|
991
|
-
}
|
|
992
|
-
function mountStreamRoutes(app, options) {
|
|
993
|
-
const {
|
|
994
|
-
db,
|
|
995
|
-
eventStreams = [],
|
|
996
|
-
accountStreams = [],
|
|
997
|
-
pathPrefix = "/api/v1"
|
|
998
|
-
} = options;
|
|
999
|
-
for (const stream of eventStreams) {
|
|
1000
|
-
if (stream.api?.enabled === false) continue;
|
|
1001
|
-
const schemas = generateSchemas(stream.table, stream.name, "Event");
|
|
1002
|
-
const filters = stream.api?.filters ?? [];
|
|
1003
|
-
const idField = stream.api?.idField ?? "id";
|
|
1004
|
-
const router = buildRoutes({
|
|
1005
|
-
db,
|
|
1006
|
-
name: stream.name,
|
|
1007
|
-
table: stream.table,
|
|
1008
|
-
schemas,
|
|
1009
|
-
filters,
|
|
1010
|
-
idField,
|
|
1011
|
-
resourceType: "event",
|
|
1012
|
-
includeSlotFilters: true,
|
|
1013
|
-
sortField: "slot",
|
|
1014
|
-
secondarySortField: "id"
|
|
1015
|
-
});
|
|
1016
|
-
app.route(`${pathPrefix}/${stream.name}`, router);
|
|
1017
|
-
}
|
|
1018
|
-
for (const stream of accountStreams) {
|
|
1019
|
-
if (stream.api?.enabled === false) continue;
|
|
1020
|
-
const schemas = generateSchemas(stream.table, stream.name, "Account");
|
|
1021
|
-
const filters = stream.api?.filters ?? [];
|
|
1022
|
-
const idField = stream.api?.idField ?? "address";
|
|
1023
|
-
const router = buildRoutes({
|
|
1024
|
-
db,
|
|
1025
|
-
name: stream.name,
|
|
1026
|
-
table: stream.table,
|
|
1027
|
-
schemas,
|
|
1028
|
-
filters,
|
|
1029
|
-
idField,
|
|
1030
|
-
resourceType: "account",
|
|
1031
|
-
includeSlotFilters: false,
|
|
1032
|
-
sortField: "updatedAt",
|
|
1033
|
-
secondarySortField: void 0
|
|
1034
|
-
});
|
|
1035
|
-
app.route(`${pathPrefix}/${stream.name}`, router);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
745
|
var Indexer = class {
|
|
1039
746
|
config;
|
|
1040
747
|
abortController = null;
|
|
@@ -1228,22 +935,13 @@ var Indexer = class {
|
|
|
1228
935
|
exports.Indexer = Indexer;
|
|
1229
936
|
exports.checkpointTable = checkpointTable;
|
|
1230
937
|
exports.columnBuilder = columnBuilder;
|
|
1231
|
-
exports.dataResponse = dataResponse;
|
|
1232
938
|
exports.defineAccountStream = defineAccountStream;
|
|
1233
939
|
exports.defineEventStream = defineEventStream;
|
|
1234
940
|
exports.deleteCheckpoint = deleteCheckpoint;
|
|
1235
|
-
exports.errorSchema = errorSchema;
|
|
1236
|
-
exports.generateSchemas = generateSchemas;
|
|
1237
941
|
exports.generateZodSchema = generateZodSchema;
|
|
1238
942
|
exports.getAllCheckpoints = getAllCheckpoints;
|
|
1239
943
|
exports.getCheckpoint = getCheckpoint;
|
|
1240
944
|
exports.getSchemaExports = getSchemaExports;
|
|
1241
|
-
exports.listResponse = listResponse;
|
|
1242
|
-
exports.mountStreamRoutes = mountStreamRoutes;
|
|
1243
|
-
exports.paginate = paginate;
|
|
1244
|
-
exports.paginationQuerySchema = paginationQuerySchema;
|
|
1245
|
-
exports.paginationResponseSchema = paginationResponseSchema;
|
|
1246
|
-
exports.parseCursor = parseCursor;
|
|
1247
945
|
exports.t = t;
|
|
1248
946
|
exports.updateCheckpoint = updateCheckpoint;
|
|
1249
947
|
exports.validateParsedData = validateParsedData;
|