@spooky-sync/client-solid 0.0.0-canary.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/.claude/settings.local.json +11 -0
- package/QUICK_START.md +415 -0
- package/README.md +330 -0
- package/dist/index.cjs +229 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +164 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
- package/src/cache/index.ts +41 -0
- package/src/cache/surrealdb-wasm-factory.ts +64 -0
- package/src/index.ts +254 -0
- package/src/lib/SpookyProvider.ts +55 -0
- package/src/lib/context.ts +13 -0
- package/src/lib/models.ts +8 -0
- package/src/lib/use-query.ts +165 -0
- package/src/types/index.ts +84 -0
- package/tsconfig.json +27 -0
- package/tsdown.config.ts +19 -0
- package/tsup.config.ts +29 -0
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spooky-sync/client-solid",
|
|
3
|
+
"version": "0.0.0-canary.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "SurrealDB client with local and remote database support for browser applications",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsdown",
|
|
19
|
+
"build:watch": "tsdown --watch",
|
|
20
|
+
"dev:example": "pnpm build:watch",
|
|
21
|
+
"dev": "tsdown --watch",
|
|
22
|
+
"test": "vitest",
|
|
23
|
+
"type-check": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"surrealdb",
|
|
27
|
+
"database",
|
|
28
|
+
"browser",
|
|
29
|
+
"offline",
|
|
30
|
+
"sync"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/mono424/spooky.git",
|
|
37
|
+
"directory": "packages/client-solid"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"packageManager": "pnpm@9.0.0",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@spooky-sync/query-builder": "workspace:*",
|
|
45
|
+
"@spooky-sync/core": "workspace:*",
|
|
46
|
+
"@surrealdb/wasm": "^3.0.0",
|
|
47
|
+
"surrealdb": "2.0.0",
|
|
48
|
+
"valtio": "^2.1.8"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"solid-js": "^1.x.x"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.0.0",
|
|
55
|
+
"solid-js": "^1.8.7",
|
|
56
|
+
"tsdown": "^0.12.4",
|
|
57
|
+
"typescript": "^5.3.0",
|
|
58
|
+
"vitest": "^1.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export { SurrealDBWasmFactory } from './surrealdb-wasm-factory';
|
|
2
|
+
|
|
3
|
+
import { Surreal } from 'surrealdb';
|
|
4
|
+
import type { CacheStrategy } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a SurrealDB WASM instance with the specified storage strategy
|
|
8
|
+
*/
|
|
9
|
+
export async function createSurrealDBWasm(
|
|
10
|
+
dbName: string,
|
|
11
|
+
strategy: CacheStrategy,
|
|
12
|
+
namespace?: string,
|
|
13
|
+
database?: string
|
|
14
|
+
): Promise<Surreal> {
|
|
15
|
+
const { SurrealDBWasmFactory } = await import('./surrealdb-wasm-factory');
|
|
16
|
+
return SurrealDBWasmFactory.create(dbName, strategy, namespace, database);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a memory-based SurrealDB WASM instance
|
|
21
|
+
*/
|
|
22
|
+
export async function createMemoryDB(
|
|
23
|
+
dbName: string,
|
|
24
|
+
namespace?: string,
|
|
25
|
+
database?: string
|
|
26
|
+
): Promise<Surreal> {
|
|
27
|
+
const { SurrealDBWasmFactory } = await import('./surrealdb-wasm-factory');
|
|
28
|
+
return SurrealDBWasmFactory.createMemory(dbName, namespace, database);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates an IndexedDB-based SurrealDB WASM instance
|
|
33
|
+
*/
|
|
34
|
+
export async function createIndexedDBDatabase(
|
|
35
|
+
dbName: string,
|
|
36
|
+
namespace?: string,
|
|
37
|
+
database?: string
|
|
38
|
+
): Promise<Surreal> {
|
|
39
|
+
const { SurrealDBWasmFactory } = await import('./surrealdb-wasm-factory');
|
|
40
|
+
return SurrealDBWasmFactory.createIndexedDB(dbName, namespace, database);
|
|
41
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Diagnostic, Surreal, applyDiagnostics } from 'surrealdb';
|
|
2
|
+
import { createWasmEngines } from '@surrealdb/wasm';
|
|
3
|
+
import type { CacheStrategy } from '../types';
|
|
4
|
+
|
|
5
|
+
const printDiagnostic = ({ key, type, phase, ...other }: Diagnostic) => {
|
|
6
|
+
if (phase === 'progress' || phase === 'after') {
|
|
7
|
+
console.log(`[SurrealDB_WASM] [${key}] ${type}:${phase}\n${JSON.stringify(other, null, 2)}`);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SurrealDB WASM client factory for different storage strategies
|
|
13
|
+
*/
|
|
14
|
+
export class SurrealDBWasmFactory {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a SurrealDB WASM instance with the specified storage strategy
|
|
17
|
+
*/
|
|
18
|
+
static async create(
|
|
19
|
+
dbName: string,
|
|
20
|
+
strategy: CacheStrategy,
|
|
21
|
+
namespace?: string,
|
|
22
|
+
database?: string
|
|
23
|
+
): Promise<Surreal> {
|
|
24
|
+
// Create Surreal instance with WASM engines
|
|
25
|
+
const surreal = new Surreal({
|
|
26
|
+
engines: applyDiagnostics(createWasmEngines(), printDiagnostic),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Connect to the appropriate storage backend
|
|
30
|
+
const connectionUrl = strategy === 'indexeddb' ? `indxdb://${dbName}` : 'mem://';
|
|
31
|
+
|
|
32
|
+
await surreal.connect(connectionUrl);
|
|
33
|
+
|
|
34
|
+
// Set namespace and database
|
|
35
|
+
await surreal.use({
|
|
36
|
+
namespace: namespace || 'main',
|
|
37
|
+
database: database || dbName,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return surreal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a memory-based SurrealDB WASM instance
|
|
45
|
+
*/
|
|
46
|
+
static async createMemory(
|
|
47
|
+
dbName: string,
|
|
48
|
+
namespace?: string,
|
|
49
|
+
database?: string
|
|
50
|
+
): Promise<Surreal> {
|
|
51
|
+
return this.create(dbName, 'memory', namespace, database);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates an IndexedDB-based SurrealDB WASM instance
|
|
56
|
+
*/
|
|
57
|
+
static async createIndexedDB(
|
|
58
|
+
dbName: string,
|
|
59
|
+
namespace?: string,
|
|
60
|
+
database?: string
|
|
61
|
+
): Promise<Surreal> {
|
|
62
|
+
return this.create(dbName, 'indexeddb', namespace, database);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { SyncedDbConfig } from './types';
|
|
2
|
+
import {
|
|
3
|
+
SpookyClient,
|
|
4
|
+
AuthService,
|
|
5
|
+
type SpookyQueryResultPromise,
|
|
6
|
+
UpdateOptions,
|
|
7
|
+
RunOptions,
|
|
8
|
+
} from '@spooky-sync/core';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
GetTable,
|
|
12
|
+
QueryBuilder,
|
|
13
|
+
SchemaStructure,
|
|
14
|
+
TableModel,
|
|
15
|
+
TableNames,
|
|
16
|
+
QueryResult,
|
|
17
|
+
RelatedFieldsMap,
|
|
18
|
+
RelationshipFieldsFromSchema,
|
|
19
|
+
GetRelationship,
|
|
20
|
+
RelatedFieldMapEntry,
|
|
21
|
+
InnerQuery,
|
|
22
|
+
BackendNames,
|
|
23
|
+
BackendRoutes,
|
|
24
|
+
RoutePayload,
|
|
25
|
+
} from '@spooky-sync/query-builder';
|
|
26
|
+
|
|
27
|
+
import { RecordId, Uuid, Surreal } from 'surrealdb';
|
|
28
|
+
export { RecordId, Uuid };
|
|
29
|
+
export type { Model, GenericModel, GenericSchema, ModelPayload } from './lib/models';
|
|
30
|
+
export { useQuery } from './lib/use-query';
|
|
31
|
+
export { SpookyProvider, type SpookyProviderProps } from './lib/SpookyProvider';
|
|
32
|
+
export { useDb } from './lib/context';
|
|
33
|
+
|
|
34
|
+
// export { AuthEventTypes } from "@spooky-sync/core"; // TODO: Verify if AuthEventTypes exists in core
|
|
35
|
+
export type {};
|
|
36
|
+
|
|
37
|
+
// Re-export query builder types for convenience
|
|
38
|
+
export type {
|
|
39
|
+
QueryModifier,
|
|
40
|
+
QueryModifierBuilder,
|
|
41
|
+
QueryInfo,
|
|
42
|
+
RelationshipsMetadata,
|
|
43
|
+
RelationshipDefinition,
|
|
44
|
+
InferRelatedModelFromMetadata,
|
|
45
|
+
GetCardinality,
|
|
46
|
+
GetTable,
|
|
47
|
+
TableModel,
|
|
48
|
+
TableNames,
|
|
49
|
+
QueryResult,
|
|
50
|
+
} from '@spooky-sync/query-builder';
|
|
51
|
+
|
|
52
|
+
export type RelationshipField<
|
|
53
|
+
Schema extends SchemaStructure,
|
|
54
|
+
TableName extends TableNames<Schema>,
|
|
55
|
+
Field extends RelationshipFieldsFromSchema<Schema, TableName>,
|
|
56
|
+
> = GetRelationship<Schema, TableName, Field>;
|
|
57
|
+
|
|
58
|
+
export type RelatedFieldsTableScoped<
|
|
59
|
+
Schema extends SchemaStructure,
|
|
60
|
+
TableName extends TableNames<Schema>,
|
|
61
|
+
RelatedFields extends RelationshipFieldsFromSchema<Schema, TableName> =
|
|
62
|
+
RelationshipFieldsFromSchema<Schema, TableName>,
|
|
63
|
+
> = {
|
|
64
|
+
[K in RelatedFields]: {
|
|
65
|
+
to: RelationshipField<Schema, TableName, K>['to'];
|
|
66
|
+
relatedFields: RelatedFieldsMap;
|
|
67
|
+
cardinality: RelationshipField<Schema, TableName, K>['cardinality'];
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type InferModel<
|
|
72
|
+
Schema extends SchemaStructure,
|
|
73
|
+
TableName extends TableNames<Schema>,
|
|
74
|
+
RelatedFields extends RelatedFieldsTableScoped<Schema, TableName>,
|
|
75
|
+
> = QueryResult<Schema, TableName, RelatedFields, true>;
|
|
76
|
+
|
|
77
|
+
export type WithRelated<Field extends string, RelatedFields extends RelatedFieldsMap = {}> = {
|
|
78
|
+
[K in Field]: Omit<RelatedFieldMapEntry, 'relatedFields'> & {
|
|
79
|
+
relatedFields: RelatedFields;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type WithRelatedMany<Field extends string, RelatedFields extends RelatedFieldsMap = {}> = {
|
|
84
|
+
[K in Field]: {
|
|
85
|
+
to: Field;
|
|
86
|
+
relatedFields: RelatedFields;
|
|
87
|
+
cardinality: 'many';
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* SyncedDb - A thin wrapper around spooky-ts for Solid.js integration
|
|
93
|
+
* Delegates all logic to the underlying spooky-ts instance
|
|
94
|
+
*/
|
|
95
|
+
export class SyncedDb<S extends SchemaStructure> {
|
|
96
|
+
private config: SyncedDbConfig<S>;
|
|
97
|
+
private spooky: SpookyClient<S> | null = null;
|
|
98
|
+
private _initialized = false;
|
|
99
|
+
|
|
100
|
+
constructor(config: SyncedDbConfig<S>) {
|
|
101
|
+
this.config = config;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public getSpooky(): SpookyClient<S> {
|
|
105
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
106
|
+
return this.spooky;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Initialize the spooky-ts instance
|
|
111
|
+
*/
|
|
112
|
+
async init(): Promise<void> {
|
|
113
|
+
if (this._initialized) return;
|
|
114
|
+
this.spooky = new SpookyClient<S>(this.config);
|
|
115
|
+
await this.spooky.init();
|
|
116
|
+
this._initialized = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create a new record in the database
|
|
121
|
+
*/
|
|
122
|
+
async create(id: string, payload: Record<string, unknown>): Promise<void> {
|
|
123
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
124
|
+
await this.spooky.create(id, payload as Record<string, unknown>);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Update an existing record in the database
|
|
129
|
+
*/
|
|
130
|
+
async update<TName extends TableNames<S>>(
|
|
131
|
+
tableName: TName,
|
|
132
|
+
recordId: string,
|
|
133
|
+
payload: Partial<TableModel<GetTable<S, TName>>>,
|
|
134
|
+
options?: UpdateOptions
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
137
|
+
await this.spooky.update(
|
|
138
|
+
tableName as string,
|
|
139
|
+
recordId,
|
|
140
|
+
payload as Record<string, unknown>,
|
|
141
|
+
options
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Delete an existing record in the database
|
|
147
|
+
*/
|
|
148
|
+
async delete<TName extends TableNames<S>>(
|
|
149
|
+
tableName: TName,
|
|
150
|
+
selector: string | InnerQuery<GetTable<S, TName>, boolean>
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
153
|
+
if (typeof selector !== 'string')
|
|
154
|
+
throw new Error('Only string ID selectors are supported currently with core');
|
|
155
|
+
await this.spooky.delete(tableName as string, selector);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Query data from the database
|
|
160
|
+
*/
|
|
161
|
+
public query<TName extends TableNames<S>>(
|
|
162
|
+
table: TName
|
|
163
|
+
): QueryBuilder<S, TName, SpookyQueryResultPromise, {}, false> {
|
|
164
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
165
|
+
return this.spooky.query(table, {});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Run a backend operation
|
|
170
|
+
*/
|
|
171
|
+
public async run<
|
|
172
|
+
B extends BackendNames<S>,
|
|
173
|
+
R extends BackendRoutes<S, B>,
|
|
174
|
+
>(
|
|
175
|
+
backend: B,
|
|
176
|
+
path: R,
|
|
177
|
+
payload: RoutePayload<S, B, R>,
|
|
178
|
+
options?: RunOptions,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
181
|
+
await this.spooky.run(backend, path, payload, options);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Authenticate with the database
|
|
186
|
+
*/
|
|
187
|
+
public async authenticate(token: string): Promise<RecordId<string>> {
|
|
188
|
+
const result = await this.spooky?.authenticate(token);
|
|
189
|
+
// SpookyClient.authenticate returns whatever remote.authenticate returns (boolean or token usually?)
|
|
190
|
+
// Wait, checked SpookyClient: return this.remote.getClient().authenticate(token);
|
|
191
|
+
// SurrealDB authenticate returns void? or token?
|
|
192
|
+
// Assuming void or token.
|
|
193
|
+
return new RecordId('user', 'me'); // Placeholder or actual?
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Deauthenticate from the database
|
|
198
|
+
* @deprecated Use signOut() instead
|
|
199
|
+
*/
|
|
200
|
+
public async deauthenticate(): Promise<void> {
|
|
201
|
+
await this.signOut();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Sign out, clear session and local storage
|
|
206
|
+
*/
|
|
207
|
+
public async signOut(): Promise<void> {
|
|
208
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
209
|
+
await this.spooky.auth.signOut();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Execute a function with direct access to the remote database connection
|
|
214
|
+
*/
|
|
215
|
+
public async useRemote<T>(fn: (db: Surreal) => T | Promise<T>): Promise<T> {
|
|
216
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
217
|
+
return await this.spooky.useRemote(fn);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Access the remote database service directly
|
|
221
|
+
*/
|
|
222
|
+
get remote(): SpookyClient<S>['remoteClient'] {
|
|
223
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
224
|
+
return this.spooky.remoteClient;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Access the local database service directly
|
|
229
|
+
*/
|
|
230
|
+
get local(): SpookyClient<S>['localClient'] {
|
|
231
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
232
|
+
return this.spooky.localClient;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Access the auth service
|
|
237
|
+
*/
|
|
238
|
+
get auth(): AuthService<S> {
|
|
239
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
240
|
+
return this.spooky.auth;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get pendingMutationCount(): number {
|
|
244
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
245
|
+
return this.spooky.pendingMutationCount;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
subscribeToPendingMutations(cb: (count: number) => void): () => void {
|
|
249
|
+
if (!this.spooky) throw new Error('SyncedDb not initialized');
|
|
250
|
+
return this.spooky.subscribeToPendingMutations(cb);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export * from './types';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createSignal, onMount, createComponent, createMemo, JSX, mergeProps } from 'solid-js';
|
|
2
|
+
import type { SchemaStructure } from '@spooky/query-builder';
|
|
3
|
+
import type { SyncedDbConfig } from '../types';
|
|
4
|
+
import { SyncedDb } from '../index';
|
|
5
|
+
import { SpookyContext } from './context';
|
|
6
|
+
|
|
7
|
+
export interface SpookyProviderProps<S extends SchemaStructure> {
|
|
8
|
+
config: SyncedDbConfig<S>;
|
|
9
|
+
fallback?: JSX.Element;
|
|
10
|
+
onError?: (error: Error) => void;
|
|
11
|
+
onReady?: (db: SyncedDb<S>) => void;
|
|
12
|
+
children: JSX.Element;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SpookyProvider<S extends SchemaStructure>(
|
|
16
|
+
props: SpookyProviderProps<S>
|
|
17
|
+
): JSX.Element {
|
|
18
|
+
const merged = mergeProps(
|
|
19
|
+
{
|
|
20
|
+
fallback: undefined as JSX.Element | undefined,
|
|
21
|
+
},
|
|
22
|
+
props
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const [db, setDb] = createSignal<SyncedDb<S> | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
onMount(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const instance = new SyncedDb<S>(merged.config);
|
|
30
|
+
await instance.init();
|
|
31
|
+
setDb(() => instance);
|
|
32
|
+
merged.onReady?.(instance);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
35
|
+
if (merged.onError) {
|
|
36
|
+
merged.onError(error);
|
|
37
|
+
} else {
|
|
38
|
+
console.error('SpookyProvider: Failed to initialize database', error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const content = createMemo(() => {
|
|
44
|
+
const instance = db();
|
|
45
|
+
if (!instance) return merged.fallback;
|
|
46
|
+
return createComponent(SpookyContext.Provider, {
|
|
47
|
+
value: instance,
|
|
48
|
+
get children() {
|
|
49
|
+
return merged.children;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return content as unknown as JSX.Element;
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createContext, useContext } from 'solid-js';
|
|
2
|
+
import type { SchemaStructure } from '@spooky/query-builder';
|
|
3
|
+
import type { SyncedDb } from '../index';
|
|
4
|
+
|
|
5
|
+
export const SpookyContext = createContext<SyncedDb<any> | undefined>();
|
|
6
|
+
|
|
7
|
+
export function useDb<S extends SchemaStructure>(): SyncedDb<S> {
|
|
8
|
+
const db = useContext(SpookyContext);
|
|
9
|
+
if (!db) {
|
|
10
|
+
throw new Error('useDb must be used within a <SpookyProvider>. Wrap your app in <SpookyProvider config={...}>.');
|
|
11
|
+
}
|
|
12
|
+
return db as SyncedDb<S>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RecordId } from 'surrealdb';
|
|
2
|
+
|
|
3
|
+
// Re-export types from query-builder for backward compatibility
|
|
4
|
+
export type { GenericModel, GenericSchema } from '@spooky/query-builder';
|
|
5
|
+
|
|
6
|
+
// Model and ModelPayload types for the client
|
|
7
|
+
export type Model<T> = T;
|
|
8
|
+
export type ModelPayload<T> = T & { id: RecordId };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ColumnSchema,
|
|
3
|
+
FinalQuery,
|
|
4
|
+
SchemaStructure,
|
|
5
|
+
TableNames,
|
|
6
|
+
QueryResult,
|
|
7
|
+
} from '@spooky-sync/query-builder';
|
|
8
|
+
import { createEffect, createSignal, onCleanup, useContext } from 'solid-js';
|
|
9
|
+
import { SyncedDb } from '..';
|
|
10
|
+
import { SpookyQueryResultPromise } from '@spooky-sync/core';
|
|
11
|
+
import { SpookyContext } from './context';
|
|
12
|
+
|
|
13
|
+
type QueryArg<
|
|
14
|
+
S extends SchemaStructure,
|
|
15
|
+
TableName extends TableNames<S>,
|
|
16
|
+
T extends { columns: Record<string, ColumnSchema> },
|
|
17
|
+
RelatedFields extends Record<string, any>,
|
|
18
|
+
IsOne extends boolean,
|
|
19
|
+
> =
|
|
20
|
+
| FinalQuery<S, TableName, T, RelatedFields, IsOne, SpookyQueryResultPromise>
|
|
21
|
+
| (() =>
|
|
22
|
+
| FinalQuery<S, TableName, T, RelatedFields, IsOne, SpookyQueryResultPromise>
|
|
23
|
+
| null
|
|
24
|
+
| undefined);
|
|
25
|
+
|
|
26
|
+
type QueryOptions = { enabled?: () => boolean };
|
|
27
|
+
|
|
28
|
+
// Overload: context-based (no explicit db)
|
|
29
|
+
export function useQuery<
|
|
30
|
+
S extends SchemaStructure,
|
|
31
|
+
TableName extends TableNames<S>,
|
|
32
|
+
T extends { columns: Record<string, ColumnSchema> },
|
|
33
|
+
RelatedFields extends Record<string, any>,
|
|
34
|
+
IsOne extends boolean,
|
|
35
|
+
TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,
|
|
36
|
+
>(
|
|
37
|
+
finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>,
|
|
38
|
+
options?: QueryOptions,
|
|
39
|
+
): { data: () => TData | undefined; error: () => Error | undefined; isLoading: () => boolean };
|
|
40
|
+
|
|
41
|
+
// Overload: explicit db (backward-compatible)
|
|
42
|
+
export function useQuery<
|
|
43
|
+
S extends SchemaStructure,
|
|
44
|
+
TableName extends TableNames<S>,
|
|
45
|
+
T extends { columns: Record<string, ColumnSchema> },
|
|
46
|
+
RelatedFields extends Record<string, any>,
|
|
47
|
+
IsOne extends boolean,
|
|
48
|
+
TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,
|
|
49
|
+
>(
|
|
50
|
+
db: SyncedDb<S>,
|
|
51
|
+
finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>,
|
|
52
|
+
options?: QueryOptions,
|
|
53
|
+
): { data: () => TData | undefined; error: () => Error | undefined; isLoading: () => boolean };
|
|
54
|
+
|
|
55
|
+
// Implementation
|
|
56
|
+
export function useQuery<
|
|
57
|
+
S extends SchemaStructure,
|
|
58
|
+
TableName extends TableNames<S>,
|
|
59
|
+
T extends {
|
|
60
|
+
columns: Record<string, ColumnSchema>;
|
|
61
|
+
},
|
|
62
|
+
RelatedFields extends Record<string, any>,
|
|
63
|
+
IsOne extends boolean,
|
|
64
|
+
TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,
|
|
65
|
+
>(
|
|
66
|
+
dbOrQuery:
|
|
67
|
+
| SyncedDb<S>
|
|
68
|
+
| QueryArg<S, TableName, T, RelatedFields, IsOne>,
|
|
69
|
+
queryOrOptions?:
|
|
70
|
+
| QueryArg<S, TableName, T, RelatedFields, IsOne>
|
|
71
|
+
| QueryOptions,
|
|
72
|
+
maybeOptions?: QueryOptions,
|
|
73
|
+
) {
|
|
74
|
+
let db: SyncedDb<S>;
|
|
75
|
+
let finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>;
|
|
76
|
+
let options: QueryOptions | undefined;
|
|
77
|
+
|
|
78
|
+
if (dbOrQuery instanceof SyncedDb) {
|
|
79
|
+
// Explicit db overload: useQuery(db, query, options?)
|
|
80
|
+
db = dbOrQuery;
|
|
81
|
+
finalQuery = queryOrOptions as QueryArg<S, TableName, T, RelatedFields, IsOne>;
|
|
82
|
+
options = maybeOptions;
|
|
83
|
+
} else {
|
|
84
|
+
// Context-based overload: useQuery(query, options?)
|
|
85
|
+
const contextDb = useContext(SpookyContext);
|
|
86
|
+
if (!contextDb) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
'useQuery: No db argument provided and no SpookyContext found. ' +
|
|
89
|
+
'Either pass a SyncedDb instance or wrap your app in <SpookyProvider>.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
db = contextDb as SyncedDb<S>;
|
|
93
|
+
finalQuery = dbOrQuery;
|
|
94
|
+
options = queryOrOptions as QueryOptions | undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [data, setData] = createSignal<TData | undefined>(undefined);
|
|
98
|
+
const [error, setError] = createSignal<Error | undefined>(undefined);
|
|
99
|
+
const [isFetched, setIsFetched] = createSignal(false);
|
|
100
|
+
const [unsubscribe, setUnsubscribe] = createSignal<(() => void) | undefined>(undefined);
|
|
101
|
+
let prevQueryString: string | undefined;
|
|
102
|
+
|
|
103
|
+
const spooky = db.getSpooky();
|
|
104
|
+
|
|
105
|
+
const initQuery = async (
|
|
106
|
+
query: FinalQuery<S, TableName, T, RelatedFields, IsOne, SpookyQueryResultPromise>
|
|
107
|
+
) => {
|
|
108
|
+
const { hash } = await query.run();
|
|
109
|
+
setError(undefined);
|
|
110
|
+
|
|
111
|
+
const unsub = await spooky.subscribe(
|
|
112
|
+
hash,
|
|
113
|
+
(e) => {
|
|
114
|
+
const data = (query.isOne ? e[0] : e) as TData;
|
|
115
|
+
setData(() => data);
|
|
116
|
+
setIsFetched(true);
|
|
117
|
+
},
|
|
118
|
+
{ immediate: true }
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
setUnsubscribe(() => unsub);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
createEffect(() => {
|
|
125
|
+
const enabled = options?.enabled?.() ?? true;
|
|
126
|
+
|
|
127
|
+
// If disabled, clear error and don't run query
|
|
128
|
+
if (!enabled) {
|
|
129
|
+
setError(undefined);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Init Query
|
|
134
|
+
const query = typeof finalQuery === 'function' ? finalQuery() : finalQuery;
|
|
135
|
+
if (!query) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Prevent re-running if query hasn't changed
|
|
140
|
+
const queryString = JSON.stringify(query);
|
|
141
|
+
if (queryString === prevQueryString) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
prevQueryString = queryString;
|
|
145
|
+
|
|
146
|
+
// Reset fetched state when query changes
|
|
147
|
+
setIsFetched(false);
|
|
148
|
+
initQuery(query);
|
|
149
|
+
|
|
150
|
+
// Cleanup
|
|
151
|
+
onCleanup(() => {
|
|
152
|
+
unsubscribe?.();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const isLoading = () => {
|
|
157
|
+
return !isFetched() && error() === undefined;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
data,
|
|
162
|
+
error,
|
|
163
|
+
isLoading,
|
|
164
|
+
};
|
|
165
|
+
}
|