@hugomrdias/foxer-client 0.0.1
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 +52 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +151 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +62 -0
- package/src/index.ts +251 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# foxer-client
|
|
2
|
+
|
|
3
|
+
`foxer-client` provides a typed Drizzle-compatible client for querying `foxer` SQL endpoints, including live updates over SSE.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Typed remote Drizzle client via `createClient`.
|
|
8
|
+
- Query compilation helper with `compileQuery`.
|
|
9
|
+
- Live query subscriptions using Server-Sent Events.
|
|
10
|
+
- Works with schema + relations for end-to-end type safety.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install foxer-client viem
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Entrypoint
|
|
19
|
+
|
|
20
|
+
- Package root: `foxer-client`
|
|
21
|
+
- Main export: `createClient`
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createClient } from 'foxer-client'
|
|
27
|
+
import { schema, relations } from './schema'
|
|
28
|
+
|
|
29
|
+
const client = createClient({
|
|
30
|
+
baseUrl: 'http://localhost:4200/sql',
|
|
31
|
+
schema,
|
|
32
|
+
relations,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const rows = await client.db.query.sessionKeys.findMany({
|
|
36
|
+
limit: 10,
|
|
37
|
+
orderBy: { blockNumber: 'desc' },
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const { unsubscribe } = client.live(
|
|
41
|
+
(db) => db.query.sessionKeys.findMany({ limit: 10 }),
|
|
42
|
+
(data) => {
|
|
43
|
+
console.log('live rows', data)
|
|
44
|
+
},
|
|
45
|
+
(error) => {
|
|
46
|
+
console.error(error)
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// later
|
|
51
|
+
unsubscribe()
|
|
52
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AnyRelations, type EmptyRelations, type QueryWithTypings, type SQLWrapper } from 'drizzle-orm';
|
|
2
|
+
import { type PgRemoteDatabase } from 'drizzle-orm/pg-proxy';
|
|
3
|
+
import type { Simplify } from 'type-fest';
|
|
4
|
+
export declare const compileQuery: (query: SQLWrapper) => QueryWithTypings;
|
|
5
|
+
type ClientDb<TSchema extends Record<string, unknown> = Record<string, unknown>, TRelations extends AnyRelations = EmptyRelations> = Simplify<Omit<PgRemoteDatabase<TSchema, TRelations>, 'insert' | 'update' | 'delete' | 'transaction' | 'refreshMaterializedView' | '_query'>>;
|
|
6
|
+
export type Client<TSchema extends Record<string, unknown> = Record<string, unknown>, TRelations extends AnyRelations = EmptyRelations> = {
|
|
7
|
+
db: ClientDb<TSchema, TRelations>;
|
|
8
|
+
live: <result>(queryFn: (db: ClientDb<TSchema, TRelations>) => Promise<result>, onData: (result: result) => void, onError?: (error: Error) => void) => {
|
|
9
|
+
unsubscribe: () => void;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export declare function createClient<TSchema extends Record<string, unknown> = Record<string, unknown>, TRelations extends AnyRelations = EmptyRelations>({ baseUrl, relations, schema, }: {
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
relations: TRelations;
|
|
15
|
+
schema: TSchema;
|
|
16
|
+
}): Client<TSchema, TRelations>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EAEjB,KAAK,cAAc,EAGnB,KAAK,gBAAgB,EAGrB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAA;AAIpB,OAAO,EAAW,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAGrE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAoDzC,eAAO,MAAM,YAAY,GAAI,OAAO,UAAU,qBAE7C,CAAA;AAED,KAAK,QAAQ,CACX,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,UAAU,SAAS,YAAY,GAAG,cAAc,IAC9C,QAAQ,CACV,IAAI,CACF,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAC,EACnC,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,aAAa,GACb,yBAAyB,GACzB,QAAQ,CACX,CACF,CAAA;AAED,MAAM,MAAM,MAAM,CAChB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,UAAU,SAAS,YAAY,GAAG,cAAc,IAC9C;IACF,EAAE,EAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;IAEjC,IAAI,EAAE,CAAC,MAAM,EACX,OAAO,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,EAC/D,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,EAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAC7B;QACH,WAAW,EAAE,MAAM,IAAI,CAAA;KACxB,CAAA;CACF,CAAA;AAED,wBAAgB,YAAY,CAC1B,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,UAAU,SAAS,YAAY,GAAG,cAAc,EAChD,EACA,OAAO,EACP,SAAS,EACT,MAAM,GACP,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,UAAU,CAAA;IACrB,MAAM,EAAE,OAAO,CAAA;CAChB,GAAG,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAsI9B"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Column, is, mapRelationalRow, SQL, Table, } from 'drizzle-orm';
|
|
2
|
+
import { PgRelationalQuery } from 'drizzle-orm/pg-core/query-builders/query';
|
|
3
|
+
import { PgRaw } from 'drizzle-orm/pg-core/query-builders/raw';
|
|
4
|
+
import { drizzle } from 'drizzle-orm/pg-proxy';
|
|
5
|
+
import { TypedQueryBuilder } from 'drizzle-orm/query-builders/query-builder';
|
|
6
|
+
import { EventSource } from 'eventsource';
|
|
7
|
+
import { stringify } from 'viem';
|
|
8
|
+
function orderSelectedFields(fields, pathPrefix) {
|
|
9
|
+
return Object.entries(fields).reduce((result, [name, field]) => {
|
|
10
|
+
if (typeof name !== 'string') {
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
const newPath = pathPrefix ? [...pathPrefix, name] : [name];
|
|
14
|
+
if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased)) {
|
|
15
|
+
result.push({ path: newPath, field });
|
|
16
|
+
}
|
|
17
|
+
else if (is(field, Table)) {
|
|
18
|
+
result.push(
|
|
19
|
+
// @ts-expect-error
|
|
20
|
+
...orderSelectedFields(field[Table.Symbol.Columns], newPath));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
result.push(...orderSelectedFields(field, newPath));
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}, []);
|
|
27
|
+
}
|
|
28
|
+
const getUrl = (baseUrl, method, query) => {
|
|
29
|
+
const url = new URL(baseUrl);
|
|
30
|
+
url.pathname = `${url.pathname}/${method}`;
|
|
31
|
+
if (query) {
|
|
32
|
+
url.searchParams.set('sql', stringify(query));
|
|
33
|
+
}
|
|
34
|
+
return url;
|
|
35
|
+
};
|
|
36
|
+
const noopDatabase = drizzle(() => Promise.resolve({ rows: [] }), {
|
|
37
|
+
casing: 'snake_case',
|
|
38
|
+
});
|
|
39
|
+
// @ts-expect-error - dialect is not typed
|
|
40
|
+
const dialect = noopDatabase.dialect;
|
|
41
|
+
export const compileQuery = (query) => {
|
|
42
|
+
return dialect.sqlToQuery(query.getSQL());
|
|
43
|
+
};
|
|
44
|
+
export function createClient({ baseUrl, relations, schema, }) {
|
|
45
|
+
return {
|
|
46
|
+
db: drizzle(async (sql, params, method, typings) => {
|
|
47
|
+
const query = { sql, params, typings };
|
|
48
|
+
const url = getUrl(baseUrl, 'db', query);
|
|
49
|
+
const rsp = await fetch(url.toString(), {
|
|
50
|
+
method: 'GET',
|
|
51
|
+
});
|
|
52
|
+
if (!rsp.ok) {
|
|
53
|
+
throw new Error((await rsp.json()).error);
|
|
54
|
+
}
|
|
55
|
+
const result = await rsp.json();
|
|
56
|
+
if (method === 'all') {
|
|
57
|
+
return {
|
|
58
|
+
...result,
|
|
59
|
+
rows: result.rows.map((row) => Object.values(row)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}, {
|
|
64
|
+
relations: relations,
|
|
65
|
+
schema: schema,
|
|
66
|
+
casing: 'snake_case',
|
|
67
|
+
}),
|
|
68
|
+
live: (queryFn, onData, onError) => {
|
|
69
|
+
// biome-ignore lint/suspicious/noExplicitAny: fix later
|
|
70
|
+
let result;
|
|
71
|
+
const passThroughDatabase = drizzle((_, __, method) => {
|
|
72
|
+
if (method === 'all') {
|
|
73
|
+
return Promise.resolve({
|
|
74
|
+
...result,
|
|
75
|
+
rows: result.rows.map((row) => Object.values(row)),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return Promise.resolve(result);
|
|
79
|
+
}, { schema: schema, relations: relations, casing: 'snake_case' });
|
|
80
|
+
const queryPromise = queryFn(passThroughDatabase);
|
|
81
|
+
if ('getSQL' in queryPromise === false) {
|
|
82
|
+
throw new Error('"queryFn" must return SQL. You may have to remove `.execute()` from your query.');
|
|
83
|
+
}
|
|
84
|
+
const queryBuilder = queryPromise;
|
|
85
|
+
const query = compileQuery(queryBuilder);
|
|
86
|
+
const sse = new EventSource(getUrl(baseUrl, 'live', query));
|
|
87
|
+
async function onMessage(event) {
|
|
88
|
+
result = JSON.parse(event.data);
|
|
89
|
+
let data;
|
|
90
|
+
if (queryBuilder instanceof TypedQueryBuilder) {
|
|
91
|
+
data = await passThroughDatabase._.session
|
|
92
|
+
.prepareQuery(query,
|
|
93
|
+
// @ts-expect-error - selectedFields is not typed
|
|
94
|
+
orderSelectedFields(queryPromise._.selectedFields), undefined, false)
|
|
95
|
+
.execute();
|
|
96
|
+
}
|
|
97
|
+
else if (queryBuilder instanceof PgRelationalQuery) {
|
|
98
|
+
// @ts-expect-error - _toSQL is not typed
|
|
99
|
+
const selection = queryBuilder._toSQL().query.selection;
|
|
100
|
+
data = await passThroughDatabase._.session
|
|
101
|
+
.prepareQuery(query, undefined, undefined, true, (rawRows, mapColumnValue) => {
|
|
102
|
+
const rows = rawRows.map((row) => {
|
|
103
|
+
const obj = {};
|
|
104
|
+
row.forEach((value, index) => {
|
|
105
|
+
// @ts-expect-error - selection is not typed
|
|
106
|
+
obj[selection[index].key] = value;
|
|
107
|
+
});
|
|
108
|
+
return mapRelationalRow(obj, selection, mapColumnValue);
|
|
109
|
+
});
|
|
110
|
+
// @ts-expect-error - mode is not typed
|
|
111
|
+
if (queryBuilder.mode === 'first') {
|
|
112
|
+
return rows[0];
|
|
113
|
+
}
|
|
114
|
+
return rows;
|
|
115
|
+
})
|
|
116
|
+
.execute();
|
|
117
|
+
}
|
|
118
|
+
else if (queryBuilder instanceof PgRaw) {
|
|
119
|
+
data = await passThroughDatabase._.session
|
|
120
|
+
.prepareQuery(query, undefined, undefined, false)
|
|
121
|
+
.execute();
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
throw new Error('Unsupported query builder');
|
|
125
|
+
}
|
|
126
|
+
// @ts-expect-error - data is not typed
|
|
127
|
+
onData(data);
|
|
128
|
+
}
|
|
129
|
+
function onSSEError(event) {
|
|
130
|
+
if ('data' in event) {
|
|
131
|
+
onError?.(new Error(event.data));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// @ts-expect-error - message is not typed
|
|
135
|
+
onError?.(new Error(`SSE error ${event?.message}`));
|
|
136
|
+
}
|
|
137
|
+
sse.close();
|
|
138
|
+
}
|
|
139
|
+
sse.addEventListener('message', onMessage);
|
|
140
|
+
sse.addEventListener('error', onSSEError);
|
|
141
|
+
return {
|
|
142
|
+
unsubscribe: () => {
|
|
143
|
+
sse.removeEventListener('message', onMessage);
|
|
144
|
+
sse.removeEventListener('error', onSSEError);
|
|
145
|
+
sse.close();
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,MAAM,EAEN,EAAE,EACF,gBAAgB,EAGhB,GAAG,EAEH,KAAK,GACN,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAA;AAC5E,OAAO,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAA;AAC9D,OAAO,EAAE,OAAO,EAAyB,MAAM,sBAAsB,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAA;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAA;AAEhC,SAAS,mBAAmB,CAC1B,MAA+B,EAC/B,UAAqB;IAErB,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAClC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE;QACxB,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,OAAO,MAAM,CAAA;QACf,CAAC;QAED,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QAC3D,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAClE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;QACvC,CAAC;aAAM,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,IAAI;YACT,mBAAmB;YACnB,GAAG,mBAAmB,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAC7D,CAAA;QACH,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CACT,GAAG,mBAAmB,CAAC,KAAgC,EAAE,OAAO,CAAC,CAClE,CAAA;QACH,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC,EACD,EAAE,CAC+B,CAAA;AACrC,CAAC;AAED,MAAM,MAAM,GAAG,CACb,OAAe,EACf,MAAqB,EACrB,KAAwB,EACxB,EAAE;IACF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;IAC5B,GAAG,CAAC,QAAQ,GAAG,GAAG,GAAG,CAAC,QAAQ,IAAI,MAAM,EAAE,CAAA;IAC1C,IAAI,KAAK,EAAE,CAAC;QACV,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;IAC/C,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE;IAChE,MAAM,EAAE,YAAY;CACrB,CAAC,CAAA;AAEF,0CAA0C;AAC1C,MAAM,OAAO,GAAc,YAAY,CAAC,OAAO,CAAA;AAE/C,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAiB,EAAE,EAAE;IAChD,OAAO,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAA;AAC3C,CAAC,CAAA;AAgCD,MAAM,UAAU,YAAY,CAG1B,EACA,OAAO,EACP,SAAS,EACT,MAAM,GAKP;IACC,OAAO;QACL,EAAE,EAAE,OAAO,CACT,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;YACrC,MAAM,KAAK,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;YACtC,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;YAExC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE;gBACtC,MAAM,EAAE,KAAK;aACd,CAAC,CAAA;YACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;YAC3C,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;YAE/B,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;gBACrB,OAAO;oBACL,GAAG,MAAM;oBACT,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;iBAC3D,CAAA;YACH,CAAC;YAED,OAAO,MAAM,CAAA;QACf,CAAC,EACD;YACE,SAAS,EAAE,SAAS;YACpB,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,YAAY;SACrB,CACF;QACD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;YACjC,wDAAwD;YACxD,IAAI,MAAW,CAAA;YACf,MAAM,mBAAmB,GAAG,OAAO,CACjC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE;gBAChB,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;oBACrB,OAAO,OAAO,CAAC,OAAO,CAAC;wBACrB,GAAG,MAAM;wBACT,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;qBAC3D,CAAC,CAAA;gBACJ,CAAC;gBAED,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;YAChC,CAAC,EACD,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,CAC/D,CAAA;YACD,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;YAEjD,IAAI,QAAQ,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;gBACvC,MAAM,IAAI,KAAK,CACb,iFAAiF,CAClF,CAAA;YACH,CAAC;YACD,MAAM,YAAY,GAAG,YAAqC,CAAA;YAE1D,MAAM,KAAK,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;YACxC,MAAM,GAAG,GAAG,IAAI,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;YAE3D,KAAK,UAAU,SAAS,CAAC,KAAmB;gBAC1C,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBAC/B,IAAI,IAAa,CAAA;gBAEjB,IAAI,YAAY,YAAY,iBAAiB,EAAE,CAAC;oBAC9C,IAAI,GAAG,MAAM,mBAAmB,CAAC,CAAC,CAAC,OAAO;yBACvC,YAAY,CACX,KAAK;oBACL,iDAAiD;oBACjD,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAAC,cAAc,CAAC,EAClD,SAAS,EACT,KAAK,CACN;yBACA,OAAO,EAAE,CAAA;gBACd,CAAC;qBAAM,IAAI,YAAY,YAAY,iBAAiB,EAAE,CAAC;oBACrD,yCAAyC;oBACzC,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,CAAA;oBAEvD,IAAI,GAAG,MAAM,mBAAmB,CAAC,CAAC,CAAC,OAAO;yBACvC,YAAY,CACX,KAAK,EACL,SAAS,EACT,SAAS,EACT,IAAI,EACJ,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE;wBAC1B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;4BAC/B,MAAM,GAAG,GAAG,EAAE,CAAA;4BACd,GAAG,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gCAC3B,4CAA4C;gCAC5C,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;4BACnC,CAAC,CAAC,CAAA;4BACF,OAAO,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,cAAc,CAAC,CAAA;wBACzD,CAAC,CAAC,CAAA;wBACF,uCAAuC;wBACvC,IAAI,YAAY,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;4BAClC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAA;wBAChB,CAAC;wBACD,OAAO,IAAI,CAAA;oBACb,CAAC,CACF;yBACA,OAAO,EAAE,CAAA;gBACd,CAAC;qBAAM,IAAI,YAAY,YAAY,KAAK,EAAE,CAAC;oBACzC,IAAI,GAAG,MAAM,mBAAmB,CAAC,CAAC,CAAC,OAAO;yBACvC,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,CAAC;yBAChD,OAAO,EAAE,CAAA;gBACd,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAA;gBAC9C,CAAC;gBAED,uCAAuC;gBACvC,MAAM,CAAC,IAAI,CAAC,CAAA;YACd,CAAC;YAED,SAAS,UAAU,CAAC,KAA2B;gBAC7C,IAAI,MAAM,IAAI,KAAK,EAAE,CAAC;oBACpB,OAAO,EAAE,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;gBAClC,CAAC;qBAAM,CAAC;oBACN,0CAA0C;oBAC1C,OAAO,EAAE,CAAC,IAAI,KAAK,CAAC,aAAa,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;gBACrD,CAAC;gBACD,GAAG,CAAC,KAAK,EAAE,CAAA;YACb,CAAC;YAED,GAAG,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;YAC1C,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;YAEzC,OAAO;gBACL,WAAW,EAAE,GAAG,EAAE;oBAChB,GAAG,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;oBAC7C,GAAG,CAAC,mBAAmB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;oBAC5C,GAAG,CAAC,KAAK,EAAE,CAAA;gBACb,CAAC;aACF,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hugomrdias/foxer-client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React hooks for interacting with Filecoin Onchain Cloud smart contracts",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/hugomrdias/foxer.git"
|
|
8
|
+
},
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"keywords": [
|
|
11
|
+
"filecoin",
|
|
12
|
+
"synapse",
|
|
13
|
+
"filecoin pay",
|
|
14
|
+
"filecoin onchain cloud",
|
|
15
|
+
"web3",
|
|
16
|
+
"react",
|
|
17
|
+
"hooks"
|
|
18
|
+
],
|
|
19
|
+
"author": "Hugo Dias <hugomrdias@gmail.com>",
|
|
20
|
+
"license": "Apache-2.0 OR MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/hugomrdias/foxer/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/hugomrdias/foxer/tree/main/packages/foxer-client",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "dist/src/index.js",
|
|
27
|
+
"module": "dist/src/index.js",
|
|
28
|
+
"types": "dist/src/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/src/index.d.ts",
|
|
32
|
+
"default": "./dist/src/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"src",
|
|
37
|
+
"dist/src",
|
|
38
|
+
"dist/src/**/*.d.ts",
|
|
39
|
+
"dist/src/**/*.d.ts.map"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc --build",
|
|
43
|
+
"lint": "tsc --build && biome check ."
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"drizzle-orm": "1.0.0-beta.15-859cf75",
|
|
47
|
+
"eventsource": "^4.1.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@biomejs/biome": "2.4.5",
|
|
51
|
+
"@types/node": "25.3.3",
|
|
52
|
+
"type-fest": "^5.4.4",
|
|
53
|
+
"typescript": "5.9.3",
|
|
54
|
+
"viem": "2.46.3"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"viem": "2.x"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyColumn,
|
|
3
|
+
type AnyRelations,
|
|
4
|
+
Column,
|
|
5
|
+
type EmptyRelations,
|
|
6
|
+
is,
|
|
7
|
+
mapRelationalRow,
|
|
8
|
+
type QueryWithTypings,
|
|
9
|
+
type SelectedFieldsOrdered,
|
|
10
|
+
SQL,
|
|
11
|
+
type SQLWrapper,
|
|
12
|
+
Table,
|
|
13
|
+
} from 'drizzle-orm'
|
|
14
|
+
import type { PgDialect } from 'drizzle-orm/pg-core'
|
|
15
|
+
import { PgRelationalQuery } from 'drizzle-orm/pg-core/query-builders/query'
|
|
16
|
+
import { PgRaw } from 'drizzle-orm/pg-core/query-builders/raw'
|
|
17
|
+
import { drizzle, type PgRemoteDatabase } from 'drizzle-orm/pg-proxy'
|
|
18
|
+
import { TypedQueryBuilder } from 'drizzle-orm/query-builders/query-builder'
|
|
19
|
+
import { EventSource } from 'eventsource'
|
|
20
|
+
import type { Simplify } from 'type-fest'
|
|
21
|
+
import { stringify } from 'viem'
|
|
22
|
+
|
|
23
|
+
function orderSelectedFields<TColumn extends AnyColumn>(
|
|
24
|
+
fields: Record<string, unknown>,
|
|
25
|
+
pathPrefix?: string[]
|
|
26
|
+
): SelectedFieldsOrdered<TColumn> {
|
|
27
|
+
return Object.entries(fields).reduce<SelectedFieldsOrdered<AnyColumn>>(
|
|
28
|
+
(result, [name, field]) => {
|
|
29
|
+
if (typeof name !== 'string') {
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const newPath = pathPrefix ? [...pathPrefix, name] : [name]
|
|
34
|
+
if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased)) {
|
|
35
|
+
result.push({ path: newPath, field })
|
|
36
|
+
} else if (is(field, Table)) {
|
|
37
|
+
result.push(
|
|
38
|
+
// @ts-expect-error
|
|
39
|
+
...orderSelectedFields(field[Table.Symbol.Columns], newPath)
|
|
40
|
+
)
|
|
41
|
+
} else {
|
|
42
|
+
result.push(
|
|
43
|
+
...orderSelectedFields(field as Record<string, unknown>, newPath)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return result
|
|
47
|
+
},
|
|
48
|
+
[]
|
|
49
|
+
) as SelectedFieldsOrdered<TColumn>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const getUrl = (
|
|
53
|
+
baseUrl: string,
|
|
54
|
+
method: 'live' | 'db',
|
|
55
|
+
query?: QueryWithTypings
|
|
56
|
+
) => {
|
|
57
|
+
const url = new URL(baseUrl)
|
|
58
|
+
url.pathname = `${url.pathname}/${method}`
|
|
59
|
+
if (query) {
|
|
60
|
+
url.searchParams.set('sql', stringify(query))
|
|
61
|
+
}
|
|
62
|
+
return url
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const noopDatabase = drizzle(() => Promise.resolve({ rows: [] }), {
|
|
66
|
+
casing: 'snake_case',
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// @ts-expect-error - dialect is not typed
|
|
70
|
+
const dialect: PgDialect = noopDatabase.dialect
|
|
71
|
+
|
|
72
|
+
export const compileQuery = (query: SQLWrapper) => {
|
|
73
|
+
return dialect.sqlToQuery(query.getSQL())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type ClientDb<
|
|
77
|
+
TSchema extends Record<string, unknown> = Record<string, unknown>,
|
|
78
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
79
|
+
> = Simplify<
|
|
80
|
+
Omit<
|
|
81
|
+
PgRemoteDatabase<TSchema, TRelations>,
|
|
82
|
+
| 'insert'
|
|
83
|
+
| 'update'
|
|
84
|
+
| 'delete'
|
|
85
|
+
| 'transaction'
|
|
86
|
+
| 'refreshMaterializedView'
|
|
87
|
+
| '_query'
|
|
88
|
+
>
|
|
89
|
+
>
|
|
90
|
+
|
|
91
|
+
export type Client<
|
|
92
|
+
TSchema extends Record<string, unknown> = Record<string, unknown>,
|
|
93
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
94
|
+
> = {
|
|
95
|
+
db: ClientDb<TSchema, TRelations>
|
|
96
|
+
|
|
97
|
+
live: <result>(
|
|
98
|
+
queryFn: (db: ClientDb<TSchema, TRelations>) => Promise<result>,
|
|
99
|
+
onData: (result: result) => void,
|
|
100
|
+
onError?: (error: Error) => void
|
|
101
|
+
) => {
|
|
102
|
+
unsubscribe: () => void
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createClient<
|
|
107
|
+
TSchema extends Record<string, unknown> = Record<string, unknown>,
|
|
108
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
109
|
+
>({
|
|
110
|
+
baseUrl,
|
|
111
|
+
relations,
|
|
112
|
+
schema,
|
|
113
|
+
}: {
|
|
114
|
+
baseUrl: string
|
|
115
|
+
relations: TRelations
|
|
116
|
+
schema: TSchema
|
|
117
|
+
}): Client<TSchema, TRelations> {
|
|
118
|
+
return {
|
|
119
|
+
db: drizzle(
|
|
120
|
+
async (sql, params, method, typings) => {
|
|
121
|
+
const query = { sql, params, typings }
|
|
122
|
+
const url = getUrl(baseUrl, 'db', query)
|
|
123
|
+
|
|
124
|
+
const rsp = await fetch(url.toString(), {
|
|
125
|
+
method: 'GET',
|
|
126
|
+
})
|
|
127
|
+
if (!rsp.ok) {
|
|
128
|
+
throw new Error((await rsp.json()).error)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const result = await rsp.json()
|
|
132
|
+
|
|
133
|
+
if (method === 'all') {
|
|
134
|
+
return {
|
|
135
|
+
...result,
|
|
136
|
+
rows: result.rows.map((row: object) => Object.values(row)),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
relations: relations,
|
|
144
|
+
schema: schema,
|
|
145
|
+
casing: 'snake_case',
|
|
146
|
+
}
|
|
147
|
+
),
|
|
148
|
+
live: (queryFn, onData, onError) => {
|
|
149
|
+
// biome-ignore lint/suspicious/noExplicitAny: fix later
|
|
150
|
+
let result: any
|
|
151
|
+
const passThroughDatabase = drizzle(
|
|
152
|
+
(_, __, method) => {
|
|
153
|
+
if (method === 'all') {
|
|
154
|
+
return Promise.resolve({
|
|
155
|
+
...result,
|
|
156
|
+
rows: result.rows.map((row: object) => Object.values(row)),
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return Promise.resolve(result)
|
|
161
|
+
},
|
|
162
|
+
{ schema: schema, relations: relations, casing: 'snake_case' }
|
|
163
|
+
)
|
|
164
|
+
const queryPromise = queryFn(passThroughDatabase)
|
|
165
|
+
|
|
166
|
+
if ('getSQL' in queryPromise === false) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
'"queryFn" must return SQL. You may have to remove `.execute()` from your query.'
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
const queryBuilder = queryPromise as unknown as SQLWrapper
|
|
172
|
+
|
|
173
|
+
const query = compileQuery(queryBuilder)
|
|
174
|
+
const sse = new EventSource(getUrl(baseUrl, 'live', query))
|
|
175
|
+
|
|
176
|
+
async function onMessage(event: MessageEvent) {
|
|
177
|
+
result = JSON.parse(event.data)
|
|
178
|
+
let data: unknown
|
|
179
|
+
|
|
180
|
+
if (queryBuilder instanceof TypedQueryBuilder) {
|
|
181
|
+
data = await passThroughDatabase._.session
|
|
182
|
+
.prepareQuery(
|
|
183
|
+
query,
|
|
184
|
+
// @ts-expect-error - selectedFields is not typed
|
|
185
|
+
orderSelectedFields(queryPromise._.selectedFields),
|
|
186
|
+
undefined,
|
|
187
|
+
false
|
|
188
|
+
)
|
|
189
|
+
.execute()
|
|
190
|
+
} else if (queryBuilder instanceof PgRelationalQuery) {
|
|
191
|
+
// @ts-expect-error - _toSQL is not typed
|
|
192
|
+
const selection = queryBuilder._toSQL().query.selection
|
|
193
|
+
|
|
194
|
+
data = await passThroughDatabase._.session
|
|
195
|
+
.prepareQuery(
|
|
196
|
+
query,
|
|
197
|
+
undefined,
|
|
198
|
+
undefined,
|
|
199
|
+
true,
|
|
200
|
+
(rawRows, mapColumnValue) => {
|
|
201
|
+
const rows = rawRows.map((row) => {
|
|
202
|
+
const obj = {}
|
|
203
|
+
row.forEach((value, index) => {
|
|
204
|
+
// @ts-expect-error - selection is not typed
|
|
205
|
+
obj[selection[index].key] = value
|
|
206
|
+
})
|
|
207
|
+
return mapRelationalRow(obj, selection, mapColumnValue)
|
|
208
|
+
})
|
|
209
|
+
// @ts-expect-error - mode is not typed
|
|
210
|
+
if (queryBuilder.mode === 'first') {
|
|
211
|
+
return rows[0]
|
|
212
|
+
}
|
|
213
|
+
return rows
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
.execute()
|
|
217
|
+
} else if (queryBuilder instanceof PgRaw) {
|
|
218
|
+
data = await passThroughDatabase._.session
|
|
219
|
+
.prepareQuery(query, undefined, undefined, false)
|
|
220
|
+
.execute()
|
|
221
|
+
} else {
|
|
222
|
+
throw new Error('Unsupported query builder')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// @ts-expect-error - data is not typed
|
|
226
|
+
onData(data)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function onSSEError(event: Event | MessageEvent) {
|
|
230
|
+
if ('data' in event) {
|
|
231
|
+
onError?.(new Error(event.data))
|
|
232
|
+
} else {
|
|
233
|
+
// @ts-expect-error - message is not typed
|
|
234
|
+
onError?.(new Error(`SSE error ${event?.message}`))
|
|
235
|
+
}
|
|
236
|
+
sse.close()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
sse.addEventListener('message', onMessage)
|
|
240
|
+
sse.addEventListener('error', onSSEError)
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
unsubscribe: () => {
|
|
244
|
+
sse.removeEventListener('message', onMessage)
|
|
245
|
+
sse.removeEventListener('error', onSSEError)
|
|
246
|
+
sse.close()
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
}
|