create-syncular-app 0.4.0 → 0.5.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-syncular-app",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
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.3.1"
52
+ "@syncular/typegen": "0.5.0"
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
  }
@@ -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/` | The schema manifest and SQL typegen's input |
50
- | `src/syncular.generated.ts` | Generated by `syncular generate` (committed) |
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>` serves both hosts. The dynamic imports keep each host's
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
@@ -0,0 +1,6 @@
1
+ query listTodos(listId) {
2
+ select id, list_id, title, done, position, updated_at_ms
3
+ from todos
4
+ where list_id = :listId
5
+ order by position, id
6
+ }
@@ -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 enough for the boot sequence in
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({ clientId: clientId(), schema });
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
- * - `useRawSql` — the live todo list; re-runs exactly when `todos`
8
- * invalidates.
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
- useRawSql,
15
+ useQuery,
16
16
  useSyncStatus,
17
17
  } from '@syncular/react';
18
- import { StrictMode, useEffect, useState } from 'react';
18
+ import { StrictMode } from 'react';
19
19
  import { createRoot } from 'react-dom/client';
20
- import { type TodosRow, todoListSubscription } from '../syncular.generated';
21
- import { createEngine, type Engine } from './engine';
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 { mutate, isPending, error } = useMutation();
45
-
46
- // Live local read: re-runs exactly when `todos` invalidates.
47
- const { rows, isLoading } = useRawSql<TodosRow>(
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 mutate([
58
- {
59
- table: 'todos',
60
- op: 'upsert',
61
- values: {
62
- id: crypto.randomUUID(),
63
- listId: LIST_ID,
64
- title,
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: TodosRow) => {
74
- void mutate([
75
- {
76
- table: 'todos',
77
- op: 'upsert',
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 mutate([{ table: 'todos', op: 'delete', rowId: id }]);
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: {String(error)}</div>
102
+ {mutation.error !== undefined ? (
103
+ <div className="error">write failed: {mutation.error.message}</div>
117
104
  ) : null}
118
105
 
119
- {isLoading ? (
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
- function Root() {
156
- const [engine, setEngine] = useState<Engine | undefined>(undefined);
157
- const [error, setError] = useState<string | undefined>(undefined);
158
-
159
- useEffect(() => {
160
- let live: Engine | undefined;
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 client={engine}>
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.4", features = ["native-transport"] }
24
+ tauri-plugin-syncular = { version = "0.5", features = ["native-transport"] }
25
25
 
26
26
  [features]
27
27
  default = []
@@ -28,7 +28,6 @@ pub fn run() {
28
28
  base_url: Some("http://localhost:8787".to_owned()),
29
29
  db_path,
30
30
  // Host-loop cadence with a little jitter (§8.4).
31
- wake_jitter_ms: 250,
32
31
  auto_sync: true,
33
32
  ..Default::default()
34
33
  };
@@ -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
  }