@spooky-sync/client-solid 0.0.1-canary.5 → 0.0.1-canary.50
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/dist/index.cjs +53 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +21 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -55
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/skills/sp00ky-solid/SKILL.md +264 -0
- package/skills/sp00ky-solid/references/file-hooks.md +112 -0
- package/src/cache/index.ts +1 -1
- package/src/cache/surrealdb-wasm-factory.ts +4 -1
- package/src/index.ts +59 -52
- package/src/lib/{SpookyProvider.ts → Sp00kyProvider.ts} +9 -7
- package/src/lib/context.ts +3 -3
- package/src/lib/models.ts +1 -1
- package/src/lib/use-download-file.ts +2 -2
- package/src/lib/use-file-upload.ts +2 -1
- package/src/lib/use-query.ts +14 -14
- package/src/types/index.ts +3 -4
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sp00ky-solid
|
|
3
|
+
description: >-
|
|
4
|
+
SolidJS integration for the Sp00ky reactive local-first SurrealDB framework.
|
|
5
|
+
Use when setting up Sp00kyProvider, using useQuery for reactive data, building
|
|
6
|
+
queries with QueryBuilder in SolidJS components, handling mutations, auth,
|
|
7
|
+
file uploads/downloads, or working with Sp00ky types like Model and RecordId.
|
|
8
|
+
metadata:
|
|
9
|
+
author: sp00ky-sync
|
|
10
|
+
version: "0.0.1"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Sp00ky SolidJS Client
|
|
14
|
+
|
|
15
|
+
`@spooky-sync/client-solid` provides SolidJS bindings for the Sp00ky framework. It wraps `@spooky-sync/core` with a context provider, reactive `useQuery` hook, and file operation hooks.
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { Sp00kyProvider } from '@spooky-sync/client-solid';
|
|
21
|
+
import { schema } from './generated/schema';
|
|
22
|
+
import schemaSurql from './generated/schema.surql?raw';
|
|
23
|
+
|
|
24
|
+
function App() {
|
|
25
|
+
return (
|
|
26
|
+
<Sp00kyProvider
|
|
27
|
+
config={{
|
|
28
|
+
database: {
|
|
29
|
+
endpoint: 'ws://localhost:8000',
|
|
30
|
+
namespace: 'my_ns',
|
|
31
|
+
database: 'my_db',
|
|
32
|
+
store: 'indexeddb',
|
|
33
|
+
},
|
|
34
|
+
schema,
|
|
35
|
+
schemaSurql,
|
|
36
|
+
logLevel: 'info',
|
|
37
|
+
}}
|
|
38
|
+
fallback={<div>Loading database...</div>}
|
|
39
|
+
onReady={(db) => console.log('DB ready')}
|
|
40
|
+
onError={(err) => console.error('DB failed', err)}
|
|
41
|
+
>
|
|
42
|
+
<MyApp />
|
|
43
|
+
</Sp00kyProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Sp00kyProvider Props
|
|
49
|
+
|
|
50
|
+
| Prop | Type | Description |
|
|
51
|
+
|------|------|-------------|
|
|
52
|
+
| `config` | `SyncedDbConfig<S>` | Same as `Sp00kyConfig` from core |
|
|
53
|
+
| `fallback` | `JSX.Element` | Shown while the database is initializing |
|
|
54
|
+
| `onReady` | `(db: SyncedDb<S>) => void` | Called when initialization succeeds |
|
|
55
|
+
| `onError` | `(error: Error) => void` | Called if initialization fails |
|
|
56
|
+
| `children` | `JSX.Element` | App content, rendered after init |
|
|
57
|
+
|
|
58
|
+
## useQuery
|
|
59
|
+
|
|
60
|
+
The primary hook for reactive data fetching. Queries automatically re-subscribe when inputs change.
|
|
61
|
+
|
|
62
|
+
### Context-based usage (recommended)
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { useQuery } from '@spooky-sync/client-solid';
|
|
66
|
+
import { QueryBuilder } from '@spooky-sync/query-builder';
|
|
67
|
+
import { schema } from './generated/schema';
|
|
68
|
+
|
|
69
|
+
function PostList() {
|
|
70
|
+
const db = useDb();
|
|
71
|
+
|
|
72
|
+
// Static query
|
|
73
|
+
const posts = useQuery(
|
|
74
|
+
db.query('post').orderBy('createdAt', 'desc').limit(20).build()
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Show when={!posts.isLoading()} fallback={<div>Loading...</div>}>
|
|
79
|
+
<For each={posts.data()}>
|
|
80
|
+
{(post) => <div>{post.title}</div>}
|
|
81
|
+
</For>
|
|
82
|
+
</Show>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Reactive queries (function form)
|
|
88
|
+
|
|
89
|
+
Wrap the query in a function to make it reactive to signal changes:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
function UserPosts(props: { userId: string }) {
|
|
93
|
+
const db = useDb();
|
|
94
|
+
|
|
95
|
+
// Query re-runs when props.userId changes
|
|
96
|
+
const posts = useQuery(
|
|
97
|
+
() => db.query('post')
|
|
98
|
+
.where({ author: props.userId })
|
|
99
|
+
.related('author')
|
|
100
|
+
.build()
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return <For each={posts.data()}>{(post) => <div>{post.title}</div>}</For>;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Conditional queries
|
|
108
|
+
|
|
109
|
+
Use the `enabled` option to conditionally run queries:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
const [userId, setUserId] = createSignal<string | null>(null);
|
|
113
|
+
|
|
114
|
+
const user = useQuery(
|
|
115
|
+
() => userId()
|
|
116
|
+
? db.query('user').where({ id: userId()! }).one().build()
|
|
117
|
+
: undefined,
|
|
118
|
+
{ enabled: () => userId() !== null }
|
|
119
|
+
);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Return value
|
|
123
|
+
|
|
124
|
+
| Property | Type | Description |
|
|
125
|
+
|----------|------|-------------|
|
|
126
|
+
| `data` | `() => T \| undefined` | Reactive accessor for query results |
|
|
127
|
+
| `error` | `() => Error \| undefined` | Reactive accessor for errors |
|
|
128
|
+
| `isLoading` | `() => boolean` | `true` until first data arrives |
|
|
129
|
+
|
|
130
|
+
### Explicit db overload
|
|
131
|
+
|
|
132
|
+
You can also pass the `SyncedDb` instance directly (legacy):
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
const posts = useQuery(db, db.query('post').build());
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## useDb
|
|
139
|
+
|
|
140
|
+
Access the `SyncedDb` instance from context:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { useDb } from '@spooky-sync/client-solid';
|
|
144
|
+
|
|
145
|
+
function MyComponent() {
|
|
146
|
+
const db = useDb();
|
|
147
|
+
// db.query(), db.create(), db.update(), db.delete(), db.auth, etc.
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Mutations
|
|
152
|
+
|
|
153
|
+
Use the `SyncedDb` instance (from `useDb()`) for mutations:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
const db = useDb();
|
|
157
|
+
|
|
158
|
+
// Create
|
|
159
|
+
await db.create('post:abc', { title: 'Hello', body: 'World', author: 'user:alice' });
|
|
160
|
+
|
|
161
|
+
// Update
|
|
162
|
+
await db.update('post', 'post:abc', { title: 'Updated' });
|
|
163
|
+
|
|
164
|
+
// Update with debounce
|
|
165
|
+
await db.update('post', 'post:abc', { body: newText }, {
|
|
166
|
+
debounced: { key: 'recordId_x_fields', delay: 300 },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Delete
|
|
170
|
+
await db.delete('post', 'post:abc');
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Authentication
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
const db = useDb();
|
|
177
|
+
|
|
178
|
+
await db.auth.signUp('user_access', { email, password, name });
|
|
179
|
+
await db.auth.signIn('user_access', { email, password });
|
|
180
|
+
await db.auth.signOut();
|
|
181
|
+
|
|
182
|
+
// Subscribe to auth state
|
|
183
|
+
const unsub = db.auth.subscribe((userId) => { ... });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## File Upload & Download
|
|
187
|
+
|
|
188
|
+
See [references/file-hooks.md](references/file-hooks.md) for details.
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import { useFileUpload, useDownloadFile } from '@spooky-sync/client-solid';
|
|
192
|
+
|
|
193
|
+
// Upload
|
|
194
|
+
const { upload, isUploading, error } = useFileUpload('avatars');
|
|
195
|
+
await upload('alice/photo.png', file);
|
|
196
|
+
|
|
197
|
+
// Download (reactive)
|
|
198
|
+
const { url, isLoading } = useDownloadFile('avatars', () => user()?.avatarPath);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Backend Runs
|
|
202
|
+
|
|
203
|
+
Use `db.run()` to trigger server-side operations via the outbox pattern. See the `sp00ky-core` skill for full details on `db.run()` and how it works.
|
|
204
|
+
|
|
205
|
+
### Basic Usage
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
const db = useDb();
|
|
209
|
+
await db.run('api', '/spookify', { id: threadId });
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Entity Linking with `assignedTo`
|
|
213
|
+
|
|
214
|
+
Pass `assignedTo` to link the job to an entity. This enables permission scoping and lets you query job status via relationships:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
const db = useDb();
|
|
218
|
+
|
|
219
|
+
// Trigger backend run linked to a thread
|
|
220
|
+
await db.run('api', '/spookify', { id: threadData.id }, {
|
|
221
|
+
assignedTo: threadData.id, // Links the job record to this thread
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Tracking Job Status Reactively
|
|
226
|
+
|
|
227
|
+
Use `.related()` to include jobs in your query, then reactively track their status:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
// Query a thread with its latest spookify job
|
|
231
|
+
const threadResult = useQuery(() =>
|
|
232
|
+
db.query('thread')
|
|
233
|
+
.where({ id: `thread:${threadId}` })
|
|
234
|
+
.related('jobs', (q) =>
|
|
235
|
+
q.where({ path: '/spookify' }).orderBy('created_at', 'desc').limit(1)
|
|
236
|
+
)
|
|
237
|
+
.one()
|
|
238
|
+
.build()
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const thread = () => threadResult.data();
|
|
242
|
+
|
|
243
|
+
// Check if a job is in progress
|
|
244
|
+
const isJobLoading = () =>
|
|
245
|
+
['pending', 'processing'].includes(thread()?.jobs?.[0]?.status ?? '');
|
|
246
|
+
|
|
247
|
+
// Use in UI
|
|
248
|
+
<Show when={isJobLoading()}>
|
|
249
|
+
<span>Processing...</span>
|
|
250
|
+
</Show>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The job's `status` field transitions through: `pending` → `processing` → `success` | `failed`. Since the job record syncs reactively, your UI updates automatically as the backend processes the job.
|
|
254
|
+
|
|
255
|
+
## Key Re-exports
|
|
256
|
+
|
|
257
|
+
The package re-exports commonly needed types:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { RecordId, Uuid } from '@spooky-sync/client-solid';
|
|
261
|
+
import type {
|
|
262
|
+
Model, GenericModel, QueryResult, TableModel, TableNames, GetTable,
|
|
263
|
+
} from '@spooky-sync/client-solid';
|
|
264
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# File Hooks Reference
|
|
2
|
+
|
|
3
|
+
## useFileUpload
|
|
4
|
+
|
|
5
|
+
Upload, download, and manage files in a SurrealDB bucket.
|
|
6
|
+
|
|
7
|
+
### Signatures
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Context-based (inside Sp00kyProvider)
|
|
11
|
+
useFileUpload<S>(bucketName: BucketNames<S>): FileUploadResult;
|
|
12
|
+
|
|
13
|
+
// Explicit db
|
|
14
|
+
useFileUpload<S>(db: SyncedDb<S>, bucketName: BucketNames<S>): FileUploadResult;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Return Value
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
interface FileUploadResult {
|
|
21
|
+
isUploading: () => boolean;
|
|
22
|
+
error: () => Error | null;
|
|
23
|
+
clearError: () => void;
|
|
24
|
+
upload: (path: string, file: File | Blob) => Promise<void>;
|
|
25
|
+
download: (path: string) => Promise<string | null>; // Returns object URL
|
|
26
|
+
remove: (path: string) => Promise<void>;
|
|
27
|
+
exists: (path: string) => Promise<boolean>;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Validation
|
|
32
|
+
|
|
33
|
+
If the bucket has `maxSize` or `allowedExtensions` configured in the schema, the hook validates files before upload and sets `error()` on failure.
|
|
34
|
+
|
|
35
|
+
### Example
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
function AvatarUpload() {
|
|
39
|
+
const { upload, isUploading, error, clearError } = useFileUpload('avatars');
|
|
40
|
+
|
|
41
|
+
const handleFile = async (e: Event) => {
|
|
42
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
43
|
+
if (file) {
|
|
44
|
+
await upload(`user/${userId()}/avatar.png`, file);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
<input type="file" onChange={handleFile} disabled={isUploading()} />
|
|
51
|
+
<Show when={error()}>
|
|
52
|
+
<p class="error">{error()!.message}</p>
|
|
53
|
+
<button onClick={clearError}>Dismiss</button>
|
|
54
|
+
</Show>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## useDownloadFile
|
|
61
|
+
|
|
62
|
+
Reactively download a file from a bucket. Re-fetches when the path changes.
|
|
63
|
+
|
|
64
|
+
### Signatures
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// Context-based
|
|
68
|
+
useDownloadFile<S>(
|
|
69
|
+
bucketName: BucketNames<S>,
|
|
70
|
+
path: Accessor<string | null | undefined>,
|
|
71
|
+
options?: { cache?: boolean },
|
|
72
|
+
): UseDownloadFileResult;
|
|
73
|
+
|
|
74
|
+
// Explicit db
|
|
75
|
+
useDownloadFile<S>(
|
|
76
|
+
db: SyncedDb<S>,
|
|
77
|
+
bucketName: BucketNames<S>,
|
|
78
|
+
path: Accessor<string | null | undefined>,
|
|
79
|
+
options?: { cache?: boolean },
|
|
80
|
+
): UseDownloadFileResult;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Return Value
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface UseDownloadFileResult {
|
|
87
|
+
url: Accessor<string | null>; // Object URL for the file
|
|
88
|
+
isLoading: Accessor<boolean>;
|
|
89
|
+
error: Accessor<Error | null>;
|
|
90
|
+
refetch: () => void; // Force re-download (evicts cache)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Caching
|
|
95
|
+
|
|
96
|
+
By default, downloads are cached by `bucket:path` key with reference counting. Object URLs are revoked when no component references them. Set `cache: false` to disable.
|
|
97
|
+
|
|
98
|
+
### Example
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
function Avatar(props: { path: string | null }) {
|
|
102
|
+
const { url, isLoading } = useDownloadFile('avatars', () => props.path);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Show when={!isLoading()} fallback={<Spinner />}>
|
|
106
|
+
<Show when={url()}>
|
|
107
|
+
<img src={url()!} alt="Avatar" />
|
|
108
|
+
</Show>
|
|
109
|
+
</Show>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
package/src/cache/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Diagnostic
|
|
1
|
+
import type { Diagnostic} from 'surrealdb';
|
|
2
|
+
import { Surreal, applyDiagnostics } from 'surrealdb';
|
|
2
3
|
import { createWasmEngines } from '@surrealdb/wasm';
|
|
3
4
|
import type { CacheStrategy } from '../types';
|
|
4
5
|
|
|
5
6
|
const printDiagnostic = ({ key, type, phase, ...other }: Diagnostic) => {
|
|
6
7
|
if (phase === 'progress' || phase === 'after') {
|
|
8
|
+
// oxlint-disable-next-line no-console -- intentional diagnostic logging
|
|
7
9
|
console.log(`[SurrealDB_WASM] [${key}] ${type}:${phase}\n${JSON.stringify(other, null, 2)}`);
|
|
8
10
|
}
|
|
9
11
|
};
|
|
@@ -11,6 +13,7 @@ const printDiagnostic = ({ key, type, phase, ...other }: Diagnostic) => {
|
|
|
11
13
|
/**
|
|
12
14
|
* SurrealDB WASM client factory for different storage strategies
|
|
13
15
|
*/
|
|
16
|
+
// oxlint-disable-next-line no-extraneous-class -- factory pattern groups related static methods
|
|
14
17
|
export class SurrealDBWasmFactory {
|
|
15
18
|
/**
|
|
16
19
|
* Creates a SurrealDB WASM instance with the specified storage strategy
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { SyncedDbConfig } from './types';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
UpdateOptions,
|
|
8
|
-
RunOptions,
|
|
3
|
+
Sp00kyClient,
|
|
4
|
+
type Sp00kyQueryResultPromise,
|
|
5
|
+
type AuthService,
|
|
6
|
+
type BucketHandle,
|
|
7
|
+
type UpdateOptions,
|
|
8
|
+
type RunOptions,
|
|
9
9
|
} from '@spooky-sync/core';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import type {
|
|
12
12
|
GetTable,
|
|
13
13
|
QueryBuilder,
|
|
14
14
|
SchemaStructure,
|
|
@@ -25,19 +25,26 @@ import {
|
|
|
25
25
|
RoutePayload,
|
|
26
26
|
BucketNames,
|
|
27
27
|
BucketDefinitionSchema,
|
|
28
|
+
QueryModifier,
|
|
29
|
+
QueryModifierBuilder,
|
|
30
|
+
QueryInfo,
|
|
31
|
+
RelationshipsMetadata,
|
|
32
|
+
RelationshipDefinition,
|
|
33
|
+
InferRelatedModelFromMetadata,
|
|
34
|
+
GetCardinality,
|
|
28
35
|
} from '@spooky-sync/query-builder';
|
|
29
36
|
|
|
30
|
-
import { RecordId, Uuid, Surreal } from 'surrealdb';
|
|
37
|
+
import { RecordId, Uuid, type Surreal } from 'surrealdb';
|
|
31
38
|
export { RecordId, Uuid };
|
|
32
39
|
export type { Model, GenericModel, GenericSchema, ModelPayload } from './lib/models';
|
|
33
40
|
export { useQuery } from './lib/use-query';
|
|
34
41
|
export { useFileUpload, type FileUploadResult } from './lib/use-file-upload';
|
|
35
42
|
export { useDownloadFile, type UseDownloadFileOptions, type UseDownloadFileResult } from './lib/use-download-file';
|
|
36
|
-
export {
|
|
43
|
+
export { Sp00kyProvider, type Sp00kyProviderProps } from './lib/Sp00kyProvider';
|
|
37
44
|
export { useDb } from './lib/context';
|
|
38
45
|
|
|
39
46
|
// export { AuthEventTypes } from "@spooky-sync/core"; // TODO: Verify if AuthEventTypes exists in core
|
|
40
|
-
|
|
47
|
+
|
|
41
48
|
|
|
42
49
|
// Re-export query builder types for convenience
|
|
43
50
|
export type {
|
|
@@ -52,7 +59,7 @@ export type {
|
|
|
52
59
|
TableModel,
|
|
53
60
|
TableNames,
|
|
54
61
|
QueryResult,
|
|
55
|
-
}
|
|
62
|
+
};
|
|
56
63
|
|
|
57
64
|
export type RelationshipField<
|
|
58
65
|
Schema extends SchemaStructure,
|
|
@@ -94,30 +101,30 @@ export type WithRelatedMany<Field extends string, RelatedFields extends RelatedF
|
|
|
94
101
|
};
|
|
95
102
|
|
|
96
103
|
/**
|
|
97
|
-
* SyncedDb - A thin wrapper around
|
|
98
|
-
* Delegates all logic to the underlying
|
|
104
|
+
* SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
|
|
105
|
+
* Delegates all logic to the underlying sp00ky-ts instance
|
|
99
106
|
*/
|
|
100
107
|
export class SyncedDb<S extends SchemaStructure> {
|
|
101
108
|
private config: SyncedDbConfig<S>;
|
|
102
|
-
private
|
|
109
|
+
private sp00ky: Sp00kyClient<S> | null = null;
|
|
103
110
|
private _initialized = false;
|
|
104
111
|
|
|
105
112
|
constructor(config: SyncedDbConfig<S>) {
|
|
106
113
|
this.config = config;
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
public
|
|
110
|
-
if (!this.
|
|
111
|
-
return this.
|
|
116
|
+
public getSp00ky(): Sp00kyClient<S> {
|
|
117
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
118
|
+
return this.sp00ky;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
/**
|
|
115
|
-
* Initialize the
|
|
122
|
+
* Initialize the sp00ky-ts instance
|
|
116
123
|
*/
|
|
117
124
|
async init(): Promise<void> {
|
|
118
125
|
if (this._initialized) return;
|
|
119
|
-
this.
|
|
120
|
-
await this.
|
|
126
|
+
this.sp00ky = new Sp00kyClient<S>(this.config);
|
|
127
|
+
await this.sp00ky.init();
|
|
121
128
|
this._initialized = true;
|
|
122
129
|
}
|
|
123
130
|
|
|
@@ -125,8 +132,8 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
125
132
|
* Create a new record in the database
|
|
126
133
|
*/
|
|
127
134
|
async create(id: string, payload: Record<string, unknown>): Promise<void> {
|
|
128
|
-
if (!this.
|
|
129
|
-
await this.
|
|
135
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
136
|
+
await this.sp00ky.create(id, payload as Record<string, unknown>);
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
/**
|
|
@@ -138,8 +145,8 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
138
145
|
payload: Partial<TableModel<GetTable<S, TName>>>,
|
|
139
146
|
options?: UpdateOptions
|
|
140
147
|
): Promise<void> {
|
|
141
|
-
if (!this.
|
|
142
|
-
await this.
|
|
148
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
149
|
+
await this.sp00ky.update(
|
|
143
150
|
tableName as string,
|
|
144
151
|
recordId,
|
|
145
152
|
payload as Record<string, unknown>,
|
|
@@ -154,10 +161,10 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
154
161
|
tableName: TName,
|
|
155
162
|
selector: string | InnerQuery<GetTable<S, TName>, boolean>
|
|
156
163
|
): Promise<void> {
|
|
157
|
-
if (!this.
|
|
164
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
158
165
|
if (typeof selector !== 'string')
|
|
159
166
|
throw new Error('Only string ID selectors are supported currently with core');
|
|
160
|
-
await this.
|
|
167
|
+
await this.sp00ky.delete(tableName as string, selector);
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
/**
|
|
@@ -165,9 +172,9 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
165
172
|
*/
|
|
166
173
|
public query<TName extends TableNames<S>>(
|
|
167
174
|
table: TName
|
|
168
|
-
): QueryBuilder<S, TName,
|
|
169
|
-
if (!this.
|
|
170
|
-
return this.
|
|
175
|
+
): QueryBuilder<S, TName, Sp00kyQueryResultPromise, {}, false> {
|
|
176
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
177
|
+
return this.sp00ky.query(table, {});
|
|
171
178
|
}
|
|
172
179
|
|
|
173
180
|
/**
|
|
@@ -182,17 +189,17 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
182
189
|
payload: RoutePayload<S, B, R>,
|
|
183
190
|
options?: RunOptions,
|
|
184
191
|
): Promise<void> {
|
|
185
|
-
if (!this.
|
|
186
|
-
await this.
|
|
192
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
193
|
+
await this.sp00ky.run(backend, path, payload, options);
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
/**
|
|
190
197
|
* Authenticate with the database
|
|
191
198
|
*/
|
|
192
199
|
public async authenticate(token: string): Promise<RecordId<string>> {
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
// Wait, checked
|
|
200
|
+
await this.sp00ky?.authenticate(token);
|
|
201
|
+
// Sp00kyClient.authenticate returns whatever remote.authenticate returns (boolean or token usually?)
|
|
202
|
+
// Wait, checked Sp00kyClient: return this.remote.getClient().authenticate(token);
|
|
196
203
|
// SurrealDB authenticate returns void? or token?
|
|
197
204
|
// Assuming void or token.
|
|
198
205
|
return new RecordId('user', 'me'); // Placeholder or actual?
|
|
@@ -210,54 +217,54 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
210
217
|
* Sign out, clear session and local storage
|
|
211
218
|
*/
|
|
212
219
|
public async signOut(): Promise<void> {
|
|
213
|
-
if (!this.
|
|
214
|
-
await this.
|
|
220
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
221
|
+
await this.sp00ky.auth.signOut();
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
/**
|
|
218
225
|
* Execute a function with direct access to the remote database connection
|
|
219
226
|
*/
|
|
220
227
|
public async useRemote<T>(fn: (db: Surreal) => T | Promise<T>): Promise<T> {
|
|
221
|
-
if (!this.
|
|
222
|
-
return await this.
|
|
228
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
229
|
+
return await this.sp00ky.useRemote(fn);
|
|
223
230
|
}
|
|
224
231
|
/**
|
|
225
232
|
* Access the remote database service directly
|
|
226
233
|
*/
|
|
227
|
-
get remote():
|
|
228
|
-
if (!this.
|
|
229
|
-
return this.
|
|
234
|
+
get remote(): Sp00kyClient<S>['remoteClient'] {
|
|
235
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
236
|
+
return this.sp00ky.remoteClient;
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
/**
|
|
233
240
|
* Access the local database service directly
|
|
234
241
|
*/
|
|
235
|
-
get local():
|
|
236
|
-
if (!this.
|
|
237
|
-
return this.
|
|
242
|
+
get local(): Sp00kyClient<S>['localClient'] {
|
|
243
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
244
|
+
return this.sp00ky.localClient;
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
/**
|
|
241
248
|
* Access the auth service
|
|
242
249
|
*/
|
|
243
250
|
get auth(): AuthService<S> {
|
|
244
|
-
if (!this.
|
|
245
|
-
return this.
|
|
251
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
252
|
+
return this.sp00ky.auth;
|
|
246
253
|
}
|
|
247
254
|
|
|
248
255
|
get pendingMutationCount(): number {
|
|
249
|
-
if (!this.
|
|
250
|
-
return this.
|
|
256
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
257
|
+
return this.sp00ky.pendingMutationCount;
|
|
251
258
|
}
|
|
252
259
|
|
|
253
260
|
subscribeToPendingMutations(cb: (count: number) => void): () => void {
|
|
254
|
-
if (!this.
|
|
255
|
-
return this.
|
|
261
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
262
|
+
return this.sp00ky.subscribeToPendingMutations(cb);
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
bucket<B extends BucketNames<S>>(name: B): BucketHandle {
|
|
259
|
-
if (!this.
|
|
260
|
-
return this.
|
|
266
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
267
|
+
return this.sp00ky.bucket(name);
|
|
261
268
|
}
|
|
262
269
|
|
|
263
270
|
getBucketConfig(name: string): BucketDefinitionSchema | undefined {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { JSX} from 'solid-js';
|
|
2
|
+
import { createSignal, onMount, createComponent, createMemo, mergeProps } from 'solid-js';
|
|
2
3
|
import type { SchemaStructure } from '@spooky/query-builder';
|
|
3
4
|
import type { SyncedDbConfig } from '../types';
|
|
4
5
|
import { SyncedDb } from '../index';
|
|
5
|
-
import {
|
|
6
|
+
import { Sp00kyContext } from './context';
|
|
6
7
|
|
|
7
|
-
export interface
|
|
8
|
+
export interface Sp00kyProviderProps<S extends SchemaStructure> {
|
|
8
9
|
config: SyncedDbConfig<S>;
|
|
9
10
|
fallback?: JSX.Element;
|
|
10
11
|
onError?: (error: Error) => void;
|
|
@@ -12,8 +13,8 @@ export interface SpookyProviderProps<S extends SchemaStructure> {
|
|
|
12
13
|
children: JSX.Element;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function
|
|
16
|
-
props:
|
|
16
|
+
export function Sp00kyProvider<S extends SchemaStructure>(
|
|
17
|
+
props: Sp00kyProviderProps<S>
|
|
17
18
|
): JSX.Element {
|
|
18
19
|
const merged = mergeProps(
|
|
19
20
|
{
|
|
@@ -35,7 +36,8 @@ export function SpookyProvider<S extends SchemaStructure>(
|
|
|
35
36
|
if (merged.onError) {
|
|
36
37
|
merged.onError(error);
|
|
37
38
|
} else {
|
|
38
|
-
|
|
39
|
+
// oxlint-disable-next-line no-console
|
|
40
|
+
console.error('Sp00kyProvider: Failed to initialize database', error);
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
});
|
|
@@ -43,7 +45,7 @@ export function SpookyProvider<S extends SchemaStructure>(
|
|
|
43
45
|
const content = createMemo(() => {
|
|
44
46
|
const instance = db();
|
|
45
47
|
if (!instance) return merged.fallback;
|
|
46
|
-
return createComponent(
|
|
48
|
+
return createComponent(Sp00kyContext.Provider, {
|
|
47
49
|
value: instance,
|
|
48
50
|
get children() {
|
|
49
51
|
return merged.children;
|