@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 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":"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"}
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
+ }