create-syncular-app 0.4.1 → 0.5.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/package.json +2 -2
- package/template/minimal/src/syncular.generated.ts +21 -0
- package/template/tauri/README.md +5 -3
- package/template/tauri/queries/list-todos.syql +6 -0
- package/template/tauri/src/frontend/engine.ts +2 -19
- package/template/tauri/src/frontend/main.tsx +49 -77
- package/template/tauri/src/syncular.generated.ts +21 -0
- package/template/tauri/src/syncular.queries.ts +99 -0
- package/template/tauri/src-tauri/Cargo.toml +1 -1
- package/template/tauri/src-tauri/src/lib.rs +0 -1
- package/template/tauri/syncular.json +3 -1
- package/template/tauri/syncular.queries.ir.json +119 -0
- package/template/web/src/syncular.generated.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-syncular-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Scaffold a new Syncular app: `create-syncular-app`",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
"!dist/**/*.test.d.ts"
|
|
50
50
|
],
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@syncular/typegen": "0.
|
|
52
|
+
"@syncular/typegen": "0.5.1"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
// irVersion: 1
|
|
3
3
|
// irHash: sha256:e2e39c94ce6c13720e1f1579e2523d460fd6bb3fea2f8eaaa114305b7aa332ba
|
|
4
4
|
|
|
5
|
+
/** Structural descriptor consumed by renderer bindings; phantom type
|
|
6
|
+
* fields make row/insert/update/id inference available without imports. */
|
|
7
|
+
export interface SyncTable<Row, Insert, Update, Id> {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/** Language-facing key used in generated row and mutation types. */
|
|
10
|
+
readonly primaryKey: keyof Row & string;
|
|
11
|
+
/** Physical SQLite/wire primary-key column. */
|
|
12
|
+
readonly physicalPrimaryKey: string;
|
|
13
|
+
readonly __row?: Row;
|
|
14
|
+
readonly __insert?: Insert;
|
|
15
|
+
readonly __update?: Update;
|
|
16
|
+
readonly __id?: Id;
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
/** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
|
|
6
20
|
export const schema = {
|
|
7
21
|
version: 1,
|
|
@@ -46,6 +60,13 @@ export interface NotesUpdate {
|
|
|
46
60
|
updatedAtMs?: number;
|
|
47
61
|
}
|
|
48
62
|
|
|
63
|
+
/** Typed mutation/resource descriptor for 'notes'. */
|
|
64
|
+
export const notesTable: SyncTable<NotesRow, NotesInsert, NotesUpdate, string> = {
|
|
65
|
+
name: 'notes',
|
|
66
|
+
primaryKey: 'id',
|
|
67
|
+
physicalPrimaryKey: 'id',
|
|
68
|
+
};
|
|
69
|
+
|
|
49
70
|
export interface NotesInListParams {
|
|
50
71
|
listId: string;
|
|
51
72
|
}
|
package/template/tauri/README.md
CHANGED
|
@@ -46,8 +46,8 @@ Prerequisites: [Rust](https://rustup.rs) and the
|
|
|
46
46
|
|
|
47
47
|
| File | What it is |
|
|
48
48
|
|---|---|
|
|
49
|
-
| `syncular.json` + `migrations/`
|
|
50
|
-
| `src/syncular.generated.ts` | Generated
|
|
49
|
+
| `syncular.json` + `migrations/` + `queries/` | Schema and named-query inputs |
|
|
50
|
+
| `src/syncular.generated.ts` + `src/syncular.queries.ts` | Generated descriptors (committed) |
|
|
51
51
|
| `src/server.ts` | Sync server + WebSocket + the web frontend, one Bun process |
|
|
52
52
|
| `src/frontend/engine.ts` | **The seam**: picks worker core vs native core |
|
|
53
53
|
| `src/frontend/main.tsx` | The shared React tree (host-agnostic) |
|
|
@@ -63,7 +63,9 @@ dynamic-imports either `createTauriSyncClient` (`@syncular/tauri` — an RPC
|
|
|
63
63
|
bridge to the native core the plugin hosts) or `createSyncClientHandle`
|
|
64
64
|
(`@syncular/client` — the worker core on OPFS). Both satisfy the one
|
|
65
65
|
structural interface the hooks target, `SyncClientLike`, so a single
|
|
66
|
-
`<SyncProvider>`
|
|
66
|
+
async client resource and `<SyncProvider>` serve both hosts. Both cores own and
|
|
67
|
+
persist client identity; app code has no localStorage identity mirror. The
|
|
68
|
+
dynamic imports keep each host's
|
|
67
69
|
machinery out of the other's bundle.
|
|
68
70
|
|
|
69
71
|
On desktop, the plugin owns the database path and the transport — see
|
|
@@ -23,15 +23,9 @@ import { schema } from '../syncular.generated';
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* What the app gets back: the hook surface (`SyncClientLike`) plus the
|
|
26
|
-
* lifecycle both concrete clients share
|
|
27
|
-
* `main.tsx`, still fully host-agnostic.
|
|
26
|
+
* lifecycle both concrete clients share, still fully host-agnostic.
|
|
28
27
|
*/
|
|
29
28
|
export interface Engine extends SyncClientLike {
|
|
30
|
-
subscribe(input: {
|
|
31
|
-
readonly id: string;
|
|
32
|
-
readonly table: string;
|
|
33
|
-
readonly scopes: Record<string, readonly string[]>;
|
|
34
|
-
}): Promise<unknown>;
|
|
35
29
|
connectRealtime(): Promise<void>;
|
|
36
30
|
close(): Promise<void>;
|
|
37
31
|
}
|
|
@@ -39,21 +33,11 @@ export interface Engine extends SyncClientLike {
|
|
|
39
33
|
/** Tauri v2 injects this into every webview it hosts. */
|
|
40
34
|
const isTauri = () => '__TAURI_INTERNALS__' in window;
|
|
41
35
|
|
|
42
|
-
/** A stable per-browser client id (the native side persists its own db). */
|
|
43
|
-
function clientId(): string {
|
|
44
|
-
const KEY = 'syncular-client-id';
|
|
45
|
-
const existing = localStorage.getItem(KEY);
|
|
46
|
-
if (existing !== null) return existing;
|
|
47
|
-
const id = crypto.randomUUID();
|
|
48
|
-
localStorage.setItem(KEY, id);
|
|
49
|
-
return id;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
36
|
export async function createEngine(): Promise<Engine> {
|
|
53
37
|
if (isTauri()) {
|
|
54
38
|
// Desktop: the native Rust core in the Tauri process.
|
|
55
39
|
const { createTauriSyncClient } = await import('@syncular/tauri');
|
|
56
|
-
return createTauriSyncClient({
|
|
40
|
+
return createTauriSyncClient({ schema });
|
|
57
41
|
}
|
|
58
42
|
// Web: the whole core in a worker, persisted on OPFS.
|
|
59
43
|
const { createSyncClientHandle } = await import('@syncular/client');
|
|
@@ -61,7 +45,6 @@ export async function createEngine(): Promise<Engine> {
|
|
|
61
45
|
return createSyncClientHandle({
|
|
62
46
|
worker: () => new Worker('/worker.js', { type: 'module' }),
|
|
63
47
|
schema,
|
|
64
|
-
clientId: clientId(),
|
|
65
48
|
database: { mode: 'persistent', name: 'app' },
|
|
66
49
|
endpoints: {
|
|
67
50
|
syncUrl: '/sync',
|
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
* the Tauri webview it talks to the native Rust core in the host process.
|
|
5
5
|
* The only host-aware code is `createEngine()` in `./engine.ts`.
|
|
6
6
|
*
|
|
7
|
-
* - `
|
|
8
|
-
*
|
|
9
|
-
* - `useMutation` — add / toggle / delete; writes go through the outbox.
|
|
7
|
+
* - generated `useQuery` — exact dependencies, coverage, revision and row key.
|
|
8
|
+
* - typed `useMutation` — add / toggle / delete through the outbox.
|
|
10
9
|
* - `useSyncStatus` — outbox depth + upgrading / schema-floor badges.
|
|
11
10
|
*/
|
|
12
11
|
import {
|
|
12
|
+
createSyncClientResource,
|
|
13
13
|
SyncProvider,
|
|
14
14
|
useMutation,
|
|
15
|
-
|
|
15
|
+
useQuery,
|
|
16
16
|
useSyncStatus,
|
|
17
17
|
} from '@syncular/react';
|
|
18
|
-
import { StrictMode
|
|
18
|
+
import { StrictMode } from 'react';
|
|
19
19
|
import { createRoot } from 'react-dom/client';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { todosTable } from '../syncular.generated';
|
|
21
|
+
import { type ListTodosRow, listTodosQuery } from '../syncular.queries';
|
|
22
|
+
import { createEngine } from './engine';
|
|
22
23
|
|
|
23
24
|
const LIST_ID = 'welcome';
|
|
24
25
|
|
|
@@ -41,47 +42,33 @@ function StatusLine() {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
function TodoApp() {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const { rows
|
|
48
|
-
'SELECT id, list_id AS listId, title, done, position,' +
|
|
49
|
-
' updated_at_ms AS updatedAtMs FROM todos WHERE list_id = ?' +
|
|
50
|
-
' ORDER BY position, id',
|
|
51
|
-
[LIST_ID],
|
|
52
|
-
);
|
|
45
|
+
const mutation = useMutation(todosTable);
|
|
46
|
+
|
|
47
|
+
const todos = useQuery(listTodosQuery, { listId: LIST_ID });
|
|
48
|
+
const { rows } = todos;
|
|
53
49
|
|
|
54
50
|
const add = (title: string) => {
|
|
55
51
|
const position =
|
|
56
52
|
rows.reduce((max, row) => Math.max(max, row.position), 0) + 1;
|
|
57
|
-
void
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
done: false,
|
|
66
|
-
position,
|
|
67
|
-
updatedAtMs: Date.now(),
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
]);
|
|
53
|
+
void mutation.upsert({
|
|
54
|
+
id: crypto.randomUUID(),
|
|
55
|
+
listId: LIST_ID,
|
|
56
|
+
title,
|
|
57
|
+
done: false,
|
|
58
|
+
position,
|
|
59
|
+
updatedAtMs: Date.now(),
|
|
60
|
+
});
|
|
71
61
|
};
|
|
72
62
|
|
|
73
|
-
const toggle = (row:
|
|
74
|
-
void
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
values: { ...row, done: !row.done, updatedAtMs: Date.now() },
|
|
79
|
-
},
|
|
80
|
-
]);
|
|
63
|
+
const toggle = (row: ListTodosRow) => {
|
|
64
|
+
void mutation.patch(row.id, {
|
|
65
|
+
done: !row.done,
|
|
66
|
+
updatedAtMs: Date.now(),
|
|
67
|
+
});
|
|
81
68
|
};
|
|
82
69
|
|
|
83
70
|
const remove = (id: string) => {
|
|
84
|
-
void
|
|
71
|
+
void mutation.remove(id);
|
|
85
72
|
};
|
|
86
73
|
|
|
87
74
|
return (
|
|
@@ -105,19 +92,21 @@ function TodoApp() {
|
|
|
105
92
|
}}
|
|
106
93
|
>
|
|
107
94
|
<input name="title" placeholder="a new todo" autoComplete="off" />
|
|
108
|
-
<button type="submit" disabled={isPending}>
|
|
95
|
+
<button type="submit" disabled={mutation.isPending}>
|
|
109
96
|
add
|
|
110
97
|
</button>
|
|
111
98
|
</form>
|
|
112
99
|
|
|
113
100
|
{/* Always render the write error: a dead worker/bridge RPC otherwise
|
|
114
101
|
fails silently and "add does nothing" is undebuggable. */}
|
|
115
|
-
{error !== undefined ? (
|
|
116
|
-
<div className="error">write failed: {
|
|
102
|
+
{mutation.error !== undefined ? (
|
|
103
|
+
<div className="error">write failed: {mutation.error.message}</div>
|
|
117
104
|
) : null}
|
|
118
105
|
|
|
119
|
-
{
|
|
106
|
+
{todos.phase === 'loading' ? (
|
|
120
107
|
<div className="empty">loading…</div>
|
|
108
|
+
) : todos.phase === 'error' ? (
|
|
109
|
+
<div className="empty">query failed: {todos.error?.message}</div>
|
|
121
110
|
) : rows.length === 0 ? (
|
|
122
111
|
<div className="empty">no todos yet — add one</div>
|
|
123
112
|
) : (
|
|
@@ -152,42 +141,25 @@ function TodoApp() {
|
|
|
152
141
|
);
|
|
153
142
|
}
|
|
154
143
|
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
void createEngine()
|
|
162
|
-
.then(async (e) => {
|
|
163
|
-
live = e;
|
|
164
|
-
await e.subscribe({
|
|
165
|
-
id: 'todos',
|
|
166
|
-
table: 'todos',
|
|
167
|
-
scopes: todoListSubscription.scopes({ listId: LIST_ID }),
|
|
168
|
-
});
|
|
169
|
-
// Ride the socket for realtime; HTTP sync still works without it.
|
|
170
|
-
try {
|
|
171
|
-
await e.connectRealtime();
|
|
172
|
-
} catch {
|
|
173
|
-
// offline / no socket — the host loop keeps syncing over HTTP
|
|
174
|
-
}
|
|
175
|
-
setEngine(e);
|
|
176
|
-
})
|
|
177
|
-
.catch((err: unknown) => setError(String(err)));
|
|
178
|
-
return () => {
|
|
179
|
-
void live?.close();
|
|
180
|
-
};
|
|
181
|
-
}, []);
|
|
182
|
-
|
|
183
|
-
if (error !== undefined) {
|
|
184
|
-
return <div className="empty">failed to start: {error}</div>;
|
|
185
|
-
}
|
|
186
|
-
if (engine === undefined) {
|
|
187
|
-
return <div className="empty">starting client core…</div>;
|
|
144
|
+
const engineResource = createSyncClientResource(async () => {
|
|
145
|
+
const engine = await createEngine();
|
|
146
|
+
try {
|
|
147
|
+
await engine.connectRealtime();
|
|
148
|
+
} catch {
|
|
149
|
+
// offline / no socket — the host loop keeps syncing over HTTP
|
|
188
150
|
}
|
|
151
|
+
return engine;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
function Root() {
|
|
189
155
|
return (
|
|
190
|
-
<SyncProvider
|
|
156
|
+
<SyncProvider
|
|
157
|
+
client={engineResource}
|
|
158
|
+
fallback={<div className="empty">starting client core…</div>}
|
|
159
|
+
renderError={(error) => (
|
|
160
|
+
<div className="empty">failed to start: {error.message}</div>
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
191
163
|
<TodoApp />
|
|
192
164
|
</SyncProvider>
|
|
193
165
|
);
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
// irVersion: 1
|
|
3
3
|
// irHash: sha256:2ffc5b70cb96cd546326de9c06868bd988c9fa3459fb3bac8428a81dcc85fca0
|
|
4
4
|
|
|
5
|
+
/** Structural descriptor consumed by renderer bindings; phantom type
|
|
6
|
+
* fields make row/insert/update/id inference available without imports. */
|
|
7
|
+
export interface SyncTable<Row, Insert, Update, Id> {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/** Language-facing key used in generated row and mutation types. */
|
|
10
|
+
readonly primaryKey: keyof Row & string;
|
|
11
|
+
/** Physical SQLite/wire primary-key column. */
|
|
12
|
+
readonly physicalPrimaryKey: string;
|
|
13
|
+
readonly __row?: Row;
|
|
14
|
+
readonly __insert?: Insert;
|
|
15
|
+
readonly __update?: Update;
|
|
16
|
+
readonly __id?: Id;
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
/** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
|
|
6
20
|
export const schema = {
|
|
7
21
|
version: 1,
|
|
@@ -54,6 +68,13 @@ export interface TodosUpdate {
|
|
|
54
68
|
updatedAtMs?: number;
|
|
55
69
|
}
|
|
56
70
|
|
|
71
|
+
/** Typed mutation/resource descriptor for 'todos'. */
|
|
72
|
+
export const todosTable: SyncTable<TodosRow, TodosInsert, TodosUpdate, string> = {
|
|
73
|
+
name: 'todos',
|
|
74
|
+
primaryKey: 'id',
|
|
75
|
+
physicalPrimaryKey: 'id',
|
|
76
|
+
};
|
|
77
|
+
|
|
57
78
|
export interface TodoListParams {
|
|
58
79
|
listId: string;
|
|
59
80
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Generated by @syncular/typegen — DO NOT EDIT.
|
|
2
|
+
// irVersion: 1
|
|
3
|
+
// irHash: sha256:4c2d58e50c85edd5a15aa6d61f2195b802e90b9056fc3cea17ceddb6ea37d0e7
|
|
4
|
+
|
|
5
|
+
/** A bindable SQL param/row value (the wrapper's SqlValue subset). */
|
|
6
|
+
export type QueryValue =
|
|
7
|
+
| string
|
|
8
|
+
| number
|
|
9
|
+
| bigint
|
|
10
|
+
| boolean
|
|
11
|
+
| Uint8Array
|
|
12
|
+
| null;
|
|
13
|
+
|
|
14
|
+
/** Minimal structural client surface — the wrapper's positional
|
|
15
|
+
* `query(sql, params?)`. `SyncClient`/`SyncClientHandle`/`SyncClientLike`
|
|
16
|
+
* all satisfy it, so this module imports nothing. */
|
|
17
|
+
export interface QueryClient {
|
|
18
|
+
query(
|
|
19
|
+
sql: string,
|
|
20
|
+
params?: readonly QueryValue[],
|
|
21
|
+
): unknown[] | Promise<unknown[]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A named-query descriptor — checked SQL plus revisioned reactive metadata and a
|
|
25
|
+
* `bind(params)` → positional args. Consumed by
|
|
26
|
+
* `@syncular/react`'s `useQuery`. `Row` is the projection row
|
|
27
|
+
* type; `Params` is `undefined` for a param-less query. `sqlFor`
|
|
28
|
+
* (present only with an orderBy knob) composes the statement from a
|
|
29
|
+
* generate-time-checked column allowlist. */
|
|
30
|
+
export interface NamedQuery<Row, Params = undefined> {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly hasParams: boolean;
|
|
33
|
+
readonly sql: string;
|
|
34
|
+
readonly tables: readonly string[];
|
|
35
|
+
readonly bind: (params: Params) => readonly QueryValue[];
|
|
36
|
+
readonly sqlFor?: (params: Params) => string;
|
|
37
|
+
readonly dependencies: (params: Params) => readonly QueryDependency[];
|
|
38
|
+
readonly coverage: (params: Params) => readonly WindowCoverage[];
|
|
39
|
+
readonly rowKey?: (row: Row) => readonly QueryValue[];
|
|
40
|
+
/** Phantom — carries the Row type for `useQuery` inference. */
|
|
41
|
+
readonly __row?: Row;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface QueryDependency {
|
|
45
|
+
readonly table: string;
|
|
46
|
+
readonly scopeKeys?: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WindowCoverage {
|
|
50
|
+
readonly base: {
|
|
51
|
+
readonly table: string;
|
|
52
|
+
readonly variable: string;
|
|
53
|
+
readonly fixedScopes?: Readonly<Record<string, readonly string[]>>;
|
|
54
|
+
readonly params?: string;
|
|
55
|
+
};
|
|
56
|
+
readonly units: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One row of the 'listTodos' query (its projection). */
|
|
60
|
+
export interface ListTodosRow {
|
|
61
|
+
id: string;
|
|
62
|
+
listId: string;
|
|
63
|
+
title: string;
|
|
64
|
+
done: boolean;
|
|
65
|
+
position: number;
|
|
66
|
+
updatedAtMs: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Named parameters for 'listTodos'. */
|
|
70
|
+
export interface ListTodosParams {
|
|
71
|
+
listId: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Tables 'listTodos' reads (compatibility/export surface). */
|
|
75
|
+
export const listTodosTables = ['todos'] as const;
|
|
76
|
+
|
|
77
|
+
const listTodosSql = 'select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos where list_id = ? order by position, id';
|
|
78
|
+
|
|
79
|
+
/** Run the 'listTodos' named query (SELECT-only). */
|
|
80
|
+
export async function listTodos(client: QueryClient, params: ListTodosParams): Promise<ListTodosRow[]> {
|
|
81
|
+
const rows = await client.query(listTodosSql, [params.listId]);
|
|
82
|
+
return rows as unknown as ListTodosRow[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Revisioned reactive descriptor for `useQuery(listTodosQuery, params)`. */
|
|
86
|
+
export const listTodosQuery: NamedQuery<ListTodosRow, ListTodosParams> = {
|
|
87
|
+
id: 'sha256:4c2d58e50c85edd5a15aa6d61f2195b802e90b9056fc3cea17ceddb6ea37d0e7/listTodos',
|
|
88
|
+
hasParams: true,
|
|
89
|
+
sql: listTodosSql,
|
|
90
|
+
tables: listTodosTables,
|
|
91
|
+
dependencies: (params) => [
|
|
92
|
+
{ table: 'todos', scopeKeys: ['list:' + String(params.listId) + ''] },
|
|
93
|
+
],
|
|
94
|
+
coverage: (params) => [
|
|
95
|
+
{ base: { table: 'todos', variable: 'list_id' }, units: [String(params.listId)] },
|
|
96
|
+
],
|
|
97
|
+
bind: (params: ListTodosParams) => [params.listId],
|
|
98
|
+
rowKey: (row) => [row.id],
|
|
99
|
+
};
|
|
@@ -21,7 +21,7 @@ tauri = { version = "2", features = [] }
|
|
|
21
21
|
# The syncular plugin from crates.io. `native-transport` gives the core its
|
|
22
22
|
# real HTTP + WebSocket transport (ureq + tungstenite) inside the host
|
|
23
23
|
# process — the webview never talks to the sync server directly.
|
|
24
|
-
tauri-plugin-syncular = { version = "0.
|
|
24
|
+
tauri-plugin-syncular = { version = "0.5", features = ["native-transport"] }
|
|
25
25
|
|
|
26
26
|
[features]
|
|
27
27
|
default = []
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"migrations": "./migrations",
|
|
4
4
|
"output": {
|
|
5
5
|
"ir": "./syncular.ir.json",
|
|
6
|
-
"module": "./src/syncular.generated.ts"
|
|
6
|
+
"module": "./src/syncular.generated.ts",
|
|
7
|
+
"queryIr": "./syncular.queries.ir.json",
|
|
8
|
+
"queries": "./src/syncular.queries.ts"
|
|
7
9
|
},
|
|
8
10
|
"schemaVersions": [{ "version": 1, "through": "0001_initial" }],
|
|
9
11
|
"tables": [{ "name": "todos", "scopes": ["list:{list_id}"] }],
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"queryIrVersion": 2,
|
|
3
|
+
"queries": [
|
|
4
|
+
{
|
|
5
|
+
"name": "listTodos",
|
|
6
|
+
"file": "list-todos.syql (query listTodos)",
|
|
7
|
+
"sourceSql": "select id, list_id, title, done, position, updated_at_ms\n from todos\n where list_id = :listId\n order by position, id",
|
|
8
|
+
"sql": "select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos where list_id = :listId order by position, id",
|
|
9
|
+
"positionalSql": "select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos where list_id = ? order by position, id",
|
|
10
|
+
"params": [
|
|
11
|
+
{
|
|
12
|
+
"name": "listId",
|
|
13
|
+
"langName": "listId",
|
|
14
|
+
"type": "string"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"columns": [
|
|
18
|
+
{
|
|
19
|
+
"name": "id",
|
|
20
|
+
"langName": "id",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"nullable": false,
|
|
23
|
+
"fidelity": "exact",
|
|
24
|
+
"origin": {
|
|
25
|
+
"table": "todos",
|
|
26
|
+
"column": "id"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "list_id",
|
|
31
|
+
"langName": "listId",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"nullable": false,
|
|
34
|
+
"fidelity": "exact",
|
|
35
|
+
"origin": {
|
|
36
|
+
"table": "todos",
|
|
37
|
+
"column": "list_id"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "title",
|
|
42
|
+
"langName": "title",
|
|
43
|
+
"type": "string",
|
|
44
|
+
"nullable": false,
|
|
45
|
+
"fidelity": "exact",
|
|
46
|
+
"origin": {
|
|
47
|
+
"table": "todos",
|
|
48
|
+
"column": "title"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "done",
|
|
53
|
+
"langName": "done",
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"nullable": false,
|
|
56
|
+
"fidelity": "exact",
|
|
57
|
+
"origin": {
|
|
58
|
+
"table": "todos",
|
|
59
|
+
"column": "done"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "position",
|
|
64
|
+
"langName": "position",
|
|
65
|
+
"type": "integer",
|
|
66
|
+
"nullable": false,
|
|
67
|
+
"fidelity": "exact",
|
|
68
|
+
"origin": {
|
|
69
|
+
"table": "todos",
|
|
70
|
+
"column": "position"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "updated_at_ms",
|
|
75
|
+
"langName": "updatedAtMs",
|
|
76
|
+
"type": "integer",
|
|
77
|
+
"nullable": false,
|
|
78
|
+
"fidelity": "exact",
|
|
79
|
+
"origin": {
|
|
80
|
+
"table": "todos",
|
|
81
|
+
"column": "updated_at_ms"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"tables": [
|
|
86
|
+
"todos"
|
|
87
|
+
],
|
|
88
|
+
"reactive": {
|
|
89
|
+
"dependencies": [
|
|
90
|
+
{
|
|
91
|
+
"table": "todos",
|
|
92
|
+
"scopes": [
|
|
93
|
+
{
|
|
94
|
+
"variable": "list_id",
|
|
95
|
+
"pattern": "list:{list_id}",
|
|
96
|
+
"params": [
|
|
97
|
+
"listId"
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"coverage": [
|
|
104
|
+
{
|
|
105
|
+
"table": "todos",
|
|
106
|
+
"variable": "list_id",
|
|
107
|
+
"units": [
|
|
108
|
+
"listId"
|
|
109
|
+
],
|
|
110
|
+
"fixedScopes": []
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"rowKey": [
|
|
114
|
+
"id"
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
// irVersion: 1
|
|
3
3
|
// irHash: sha256:2ffc5b70cb96cd546326de9c06868bd988c9fa3459fb3bac8428a81dcc85fca0
|
|
4
4
|
|
|
5
|
+
/** Structural descriptor consumed by renderer bindings; phantom type
|
|
6
|
+
* fields make row/insert/update/id inference available without imports. */
|
|
7
|
+
export interface SyncTable<Row, Insert, Update, Id> {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/** Language-facing key used in generated row and mutation types. */
|
|
10
|
+
readonly primaryKey: keyof Row & string;
|
|
11
|
+
/** Physical SQLite/wire primary-key column. */
|
|
12
|
+
readonly physicalPrimaryKey: string;
|
|
13
|
+
readonly __row?: Row;
|
|
14
|
+
readonly __insert?: Insert;
|
|
15
|
+
readonly __update?: Update;
|
|
16
|
+
readonly __id?: Id;
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
/** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
|
|
6
20
|
export const schema = {
|
|
7
21
|
version: 1,
|
|
@@ -54,6 +68,13 @@ export interface TodosUpdate {
|
|
|
54
68
|
updatedAtMs?: number;
|
|
55
69
|
}
|
|
56
70
|
|
|
71
|
+
/** Typed mutation/resource descriptor for 'todos'. */
|
|
72
|
+
export const todosTable: SyncTable<TodosRow, TodosInsert, TodosUpdate, string> = {
|
|
73
|
+
name: 'todos',
|
|
74
|
+
primaryKey: 'id',
|
|
75
|
+
physicalPrimaryKey: 'id',
|
|
76
|
+
};
|
|
77
|
+
|
|
57
78
|
export interface TodoListParams {
|
|
58
79
|
listId: string;
|
|
59
80
|
}
|