create-syncular-app 0.1.3 → 0.2.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.
Files changed (64) hide show
  1. package/README.md +70 -27
  2. package/dist/cli.d.ts +11 -0
  3. package/dist/cli.js +131 -179
  4. package/dist/constants.d.ts +40 -0
  5. package/dist/constants.js +46 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.js +3 -0
  8. package/dist/scaffold.d.ts +44 -0
  9. package/dist/scaffold.js +106 -0
  10. package/package.json +22 -15
  11. package/src/cli.ts +174 -0
  12. package/src/constants.ts +53 -0
  13. package/src/index.ts +3 -0
  14. package/src/scaffold.ts +161 -0
  15. package/template/minimal/README.md +45 -0
  16. package/template/minimal/gitignore +5 -0
  17. package/template/minimal/migrations/0001_initial/up.sql +9 -0
  18. package/template/minimal/package.json +22 -0
  19. package/template/minimal/src/clients.ts +54 -0
  20. package/template/minimal/src/make-client.ts +26 -0
  21. package/template/minimal/src/server.ts +39 -0
  22. package/template/minimal/src/smoke.test.ts +70 -0
  23. package/template/minimal/src/syncular.generated.ts +66 -0
  24. package/template/minimal/syncular.ir.json +66 -0
  25. package/template/minimal/syncular.json +17 -0
  26. package/template/minimal/tsconfig.json +16 -0
  27. package/template/web/README.md +63 -0
  28. package/template/web/gitignore +5 -0
  29. package/template/web/migrations/0001_initial/up.sql +11 -0
  30. package/template/web/package.json +22 -0
  31. package/template/web/src/frontend/index.html +37 -0
  32. package/template/web/src/frontend/main.ts +191 -0
  33. package/template/web/src/frontend/worker.ts +9 -0
  34. package/template/web/src/server.ts +183 -0
  35. package/template/web/src/smoke.test.ts +91 -0
  36. package/template/web/src/syncular.generated.ts +74 -0
  37. package/template/web/syncular.ir.json +76 -0
  38. package/template/web/syncular.json +17 -0
  39. package/template/web/tsconfig.json +16 -0
  40. package/template/README.md +0 -122
  41. package/template/_gitignore +0 -5
  42. package/template/generated/kotlin/SyncularApp.kt +0 -1005
  43. package/template/generated/rust/diesel_tables.rs +0 -142
  44. package/template/generated/rust/migrations.rs +0 -32
  45. package/template/generated/rust/schema.rs +0 -15
  46. package/template/generated/rust/syncular.rs +0 -926
  47. package/template/generated/swift/SyncularApp.swift +0 -1191
  48. package/template/generated/syncular.codegen.json +0 -19
  49. package/template/index.html +0 -13
  50. package/template/migrations/0001_initial/down.sql +0 -1
  51. package/template/migrations/0001_initial/up.sql +0 -8
  52. package/template/package.json +0 -33
  53. package/template/scripts/dev.ts +0 -42
  54. package/template/src/app.tsx +0 -231
  55. package/template/src/client/syncular.ts +0 -61
  56. package/template/src/generated/syncular.generated.ts +0 -769
  57. package/template/src/generated/syncular.server.generated.ts +0 -512
  58. package/template/src/main.tsx +0 -5
  59. package/template/src/server/sync-server.ts +0 -129
  60. package/template/src/styles.css +0 -233
  61. package/template/syncular.app.ts +0 -23
  62. package/template/syncular.schema.json +0 -145
  63. package/template/tsconfig.json +0 -18
  64. package/template/vite.config.ts +0 -9
@@ -0,0 +1,191 @@
1
+ /**
2
+ * A single-pane browser todo app: one client core syncing through the server.
3
+ *
4
+ * The WHOLE core runs in a Web Worker on a persistent opfs-sahpool database,
5
+ * driven through the `SyncClientHandle` RPC — the page only sends
6
+ * query/mutate/subscribe messages and re-renders. This is the real browser
7
+ * shape; grow your app from here.
8
+ *
9
+ * Open two browser windows on the same URL to watch them converge over the
10
+ * realtime socket. (One core per origin: a second TAB gets a clear not-leader
11
+ * error — multi-tab followers are a roadmap item. Separate windows/profiles or
12
+ * the two-window trick show convergence today.)
13
+ */
14
+ import {
15
+ ClientSyncError,
16
+ createSyncClientHandle,
17
+ NOT_LEADER_CODE,
18
+ type SqlRow,
19
+ } from '@syncular/client';
20
+ import { schema, todoListSubscription } from '../syncular.generated';
21
+
22
+ const LIST_ID = 'welcome';
23
+ const WS_PROTO = location.protocol === 'https:' ? 'wss' : 'ws';
24
+
25
+ interface Todo {
26
+ id: string;
27
+ list_id: string;
28
+ title: string;
29
+ done: number | boolean;
30
+ position: number;
31
+ updated_at_ms: number;
32
+ }
33
+
34
+ const statusEl = document.getElementById('status') as HTMLElement;
35
+ const listEl = document.getElementById('todos') as HTMLUListElement;
36
+ const outboxEl = document.getElementById('outbox') as HTMLElement;
37
+ const form = document.getElementById('add-form') as HTMLFormElement;
38
+ const input = document.getElementById('add-input') as HTMLInputElement;
39
+ const offlineBtn = document.getElementById('offline-btn') as HTMLButtonElement;
40
+
41
+ function setStatus(text: string): void {
42
+ statusEl.textContent = text;
43
+ }
44
+
45
+ async function main(): Promise<void> {
46
+ const handle = await createSyncClientHandle({
47
+ worker: () => new Worker('/worker.js', { type: 'module' }),
48
+ schema,
49
+ database: { mode: 'persistent', name: 'app' },
50
+ endpoints: {
51
+ syncUrl: '/sync',
52
+ segmentsUrl: '/segments',
53
+ realtimeUrl: `${WS_PROTO}://${location.host}/realtime?clientId={clientId}`,
54
+ },
55
+ autoSync: true,
56
+ lockName: 'app-core',
57
+ onSynced: () => void refresh(),
58
+ });
59
+
60
+ if (!handle.isLeader) {
61
+ throw new ClientSyncError(
62
+ NOT_LEADER_CODE,
63
+ 'another tab already owns this app’s core — close it first ' +
64
+ '(multi-tab followers are a roadmap item).',
65
+ );
66
+ }
67
+
68
+ await handle.subscribe({
69
+ id: 'todos',
70
+ table: 'todos',
71
+ scopes: todoListSubscription.scopes({ listId: LIST_ID }),
72
+ });
73
+
74
+ // Connect-then-sync (§8.7 reference boot order): the first sync round rides
75
+ // the socket and registers this connection's subscriptions at round end.
76
+ try {
77
+ await handle.connectRealtime();
78
+ } catch {
79
+ // HTTP sync still works without the socket.
80
+ }
81
+ await handle.syncUntilIdle();
82
+ setStatus('in sync');
83
+ await refresh();
84
+
85
+ let offline = false;
86
+ offlineBtn.addEventListener('click', async () => {
87
+ offline = !offline;
88
+ await handle.setOffline(offline);
89
+ offlineBtn.textContent = offline ? 'Go online' : 'Go offline';
90
+ if (offline) {
91
+ setStatus('offline — edits queue in the outbox');
92
+ } else {
93
+ setStatus('back online — draining outbox…');
94
+ try {
95
+ await handle.connectRealtime();
96
+ } catch {
97
+ // socket optional
98
+ }
99
+ await handle.syncUntilIdle();
100
+ setStatus('in sync');
101
+ }
102
+ await refresh();
103
+ });
104
+
105
+ form.addEventListener('submit', async (event) => {
106
+ event.preventDefault();
107
+ const title = input.value.trim();
108
+ if (title === '') return;
109
+ input.value = '';
110
+ const rows = (await handle.query(
111
+ 'SELECT position FROM todos ORDER BY position DESC LIMIT 1',
112
+ )) as SqlRow[];
113
+ const top = rows[0]?.position;
114
+ const position = (typeof top === 'number' ? top : 0) + 1;
115
+ await handle.mutate([
116
+ {
117
+ table: 'todos',
118
+ op: 'upsert',
119
+ values: {
120
+ id: crypto.randomUUID(),
121
+ list_id: LIST_ID,
122
+ title,
123
+ done: false,
124
+ position,
125
+ updated_at_ms: Date.now(),
126
+ },
127
+ },
128
+ ]);
129
+ if (!offline) await handle.syncUntilIdle();
130
+ await refresh();
131
+ });
132
+
133
+ async function refresh(): Promise<void> {
134
+ const rows = (await handle.query(
135
+ 'SELECT * FROM todos ORDER BY position ASC, id ASC',
136
+ )) as unknown as Todo[];
137
+ const pending = (await handle.pendingCommits()).length;
138
+ outboxEl.textContent = `outbox ${pending}`;
139
+ listEl.replaceChildren(...rows.map((row) => renderRow(row)));
140
+
141
+ async function toggle(row: Todo): Promise<void> {
142
+ await handle.mutate([
143
+ {
144
+ table: 'todos',
145
+ op: 'upsert',
146
+ values: {
147
+ id: row.id,
148
+ list_id: row.list_id,
149
+ title: row.title,
150
+ done: !row.done,
151
+ position: row.position,
152
+ updated_at_ms: Date.now(),
153
+ },
154
+ },
155
+ ]);
156
+ if (!offline) await handle.syncUntilIdle();
157
+ await refresh();
158
+ }
159
+ async function remove(id: string): Promise<void> {
160
+ await handle.mutate([{ table: 'todos', op: 'delete', rowId: id }]);
161
+ if (!offline) await handle.syncUntilIdle();
162
+ await refresh();
163
+ }
164
+
165
+ function renderRow(row: Todo): HTMLLIElement {
166
+ const li = document.createElement('li');
167
+ if (row.done) li.classList.add('done');
168
+ const checkbox = document.createElement('input');
169
+ checkbox.type = 'checkbox';
170
+ checkbox.checked = Boolean(row.done);
171
+ checkbox.addEventListener('change', () => void toggle(row));
172
+ const title = document.createElement('span');
173
+ title.className = 'title';
174
+ title.textContent = row.title;
175
+ const del = document.createElement('button');
176
+ del.textContent = '×';
177
+ del.title = 'delete';
178
+ del.addEventListener('click', () => void remove(row.id));
179
+ li.append(checkbox, title, del);
180
+ return li;
181
+ }
182
+ }
183
+ }
184
+
185
+ void main().catch((error: unknown) => {
186
+ const message =
187
+ error instanceof ClientSyncError
188
+ ? `${error.code}: ${error.message}`
189
+ : String(error);
190
+ setStatus(`failed to start — ${message}`);
191
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The sync worker: the whole client core (SyncClient + transports + sqlite-wasm
3
+ * on opfs-sahpool) runs here — the page only talks RPC. Built as its own
4
+ * bundle (`/worker.js`) because module workers do not inherit the page's import
5
+ * map.
6
+ */
7
+ import { startSyncWorker } from '@syncular/client/worker';
8
+
9
+ startSyncWorker();
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The whole app in one Bun process:
3
+ * - POST /sync + GET /segments/:id via the server-hono adapter,
4
+ * - GET /realtime as a WebSocket wired to the server's RealtimeHub (§8.7),
5
+ * - the static frontend: TWO bundles built with Bun.build at startup —
6
+ * /app.js (the page) and /worker.js (the sync worker running the whole
7
+ * client core on opfs-sahpool). Module workers do not inherit the page's
8
+ * import map, so the sqlite-wasm bare specifier is rewritten to the served
9
+ * vendor path in both bundles.
10
+ *
11
+ * Storage is bun:sqlite (in-memory by default; set DB_PATH=path for a file).
12
+ *
13
+ * EDIT FIRST: `resolveScopes` + `authenticate` below are the whole
14
+ * authorization story — they run in YOUR backend next to YOUR auth.
15
+ */
16
+ import { dirname, join } from 'node:path';
17
+ import {
18
+ createRealtimeHub,
19
+ MemorySegmentStore,
20
+ type RealtimeSession,
21
+ SqliteServerStorage,
22
+ type SyncServerConfig,
23
+ } from '@syncular/server';
24
+ import { createSyncularHono } from '@syncular/server-hono';
25
+ import { schema } from './syncular.generated';
26
+
27
+ const PORT = Number(process.env.PORT ?? 8787);
28
+ const PARTITION = 'demo';
29
+ const ACTOR_ID = 'demo-user';
30
+
31
+ // -- sync server ------------------------------------------------------------
32
+
33
+ const storage = new SqliteServerStorage(process.env.DB_PATH ?? ':memory:');
34
+ const segments = new MemorySegmentStore();
35
+ /** Demo authorization: the single actor may see every list. Replace with the
36
+ * scope values the authenticated actor is allowed to see. */
37
+ const resolveScopes = () => ({ list_id: ['*'] });
38
+
39
+ const hub = createRealtimeHub({ schema, storage, resolveScopes, segments });
40
+ const config: SyncServerConfig = {
41
+ schema,
42
+ storage,
43
+ segments,
44
+ resolveScopes,
45
+ realtime: hub,
46
+ };
47
+ const hono = createSyncularHono({
48
+ config,
49
+ // Replace with your real auth: return { actorId, partition } or null (401).
50
+ authenticate: async () => ({ actorId: ACTOR_ID, partition: PARTITION }),
51
+ });
52
+
53
+ // -- frontend build + static assets ------------------------------------------
54
+
55
+ const build = await Bun.build({
56
+ entrypoints: [
57
+ join(import.meta.dir, 'frontend', 'main.ts'),
58
+ join(import.meta.dir, 'frontend', 'worker.ts'),
59
+ ],
60
+ target: 'browser',
61
+ sourcemap: 'inline',
62
+ external: ['@sqlite.org/sqlite-wasm'],
63
+ });
64
+ async function bundleText(basename: string): Promise<string> {
65
+ const artifact = build.outputs.find((output) =>
66
+ output.path.endsWith(`/${basename}`),
67
+ );
68
+ if (artifact === undefined) {
69
+ throw new Error(`frontend build produced no ${basename}`);
70
+ }
71
+ // Both bundles import sqlite-wasm from the served vendor path. An import map
72
+ // would only cover the page, never the module worker, so the external bare
73
+ // specifier is rewritten in the emitted JS instead.
74
+ const text = await artifact.text();
75
+ return text.replaceAll(
76
+ /(["'])@sqlite\.org\/sqlite-wasm\1/g,
77
+ '"/vendor/sqlite-wasm/index.mjs"',
78
+ );
79
+ }
80
+ const appJs = await bundleText('main.js');
81
+ const workerJs = await bundleText('worker.js');
82
+ const indexHtml = await Bun.file(
83
+ join(import.meta.dir, 'frontend', 'index.html'),
84
+ ).text();
85
+
86
+ const wasmDir = dirname(
87
+ Bun.resolveSync('@sqlite.org/sqlite-wasm', import.meta.dir),
88
+ );
89
+ /** Only the files the sqlite-wasm ESM entry actually references. */
90
+ const WASM_FILES: Record<string, string> = {
91
+ 'index.mjs': 'text/javascript',
92
+ 'sqlite3.wasm': 'application/wasm',
93
+ 'sqlite3-opfs-async-proxy.js': 'text/javascript',
94
+ 'sqlite3-worker1.mjs': 'text/javascript',
95
+ };
96
+
97
+ /** COOP/COEP so OPFS-capable contexts get cross-origin isolation (not
98
+ * required by opfs-sahpool, but harmless and future-proof). */
99
+ const STATIC_HEADERS = {
100
+ 'Cross-Origin-Opener-Policy': 'same-origin',
101
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
102
+ 'Cache-Control': 'no-store',
103
+ };
104
+
105
+ function staticResponse(body: string | Uint8Array, type: string): Response {
106
+ return new Response(body as BodyInit, {
107
+ headers: { ...STATIC_HEADERS, 'Content-Type': type },
108
+ });
109
+ }
110
+
111
+ // -- one process, one port ----------------------------------------------------
112
+
113
+ interface SocketData {
114
+ clientId: string;
115
+ session?: RealtimeSession;
116
+ }
117
+
118
+ const server = Bun.serve<SocketData, never>({
119
+ port: PORT,
120
+ async fetch(request, bunServer) {
121
+ const url = new URL(request.url);
122
+ const path = url.pathname;
123
+
124
+ if (path === '/realtime') {
125
+ const clientId = url.searchParams.get('clientId') ?? crypto.randomUUID();
126
+ if (bunServer.upgrade(request, { data: { clientId } })) {
127
+ return undefined as unknown as Response;
128
+ }
129
+ return new Response('expected a websocket upgrade', { status: 400 });
130
+ }
131
+ if (path === '/sync' || path.startsWith('/segments/')) {
132
+ return hono.fetch(request);
133
+ }
134
+ if (path === '/' || path === '/index.html') {
135
+ return staticResponse(indexHtml, 'text/html; charset=utf-8');
136
+ }
137
+ if (path === '/app.js') {
138
+ return staticResponse(appJs, 'text/javascript; charset=utf-8');
139
+ }
140
+ if (path === '/worker.js') {
141
+ return staticResponse(workerJs, 'text/javascript; charset=utf-8');
142
+ }
143
+ if (path.startsWith('/vendor/sqlite-wasm/')) {
144
+ const name = path.slice('/vendor/sqlite-wasm/'.length);
145
+ const type = WASM_FILES[name];
146
+ if (type !== undefined) {
147
+ const bytes = await Bun.file(join(wasmDir, name)).bytes();
148
+ return staticResponse(bytes, type);
149
+ }
150
+ }
151
+ return new Response('not found', { status: 404 });
152
+ },
153
+ websocket: {
154
+ open(ws) {
155
+ hub
156
+ .connect({
157
+ partition: PARTITION,
158
+ actorId: ACTOR_ID,
159
+ clientId: ws.data.clientId,
160
+ send: (data) => {
161
+ ws.send(data);
162
+ },
163
+ closeSocket: () => ws.close(1008, 'protocol violation (§8.7)'),
164
+ })
165
+ .then((session) => {
166
+ ws.data.session = session;
167
+ })
168
+ .catch(() => ws.close(1011, 'realtime connect failed'));
169
+ },
170
+ message(ws, message) {
171
+ if (typeof message === 'string') {
172
+ ws.data.session?.handleMessage(message);
173
+ } else {
174
+ ws.data.session?.handleBinary(new Uint8Array(message));
175
+ }
176
+ },
177
+ close(ws) {
178
+ ws.data.session?.close();
179
+ },
180
+ },
181
+ });
182
+
183
+ console.log(`app: http://localhost:${server.port}`);
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The web template's smoke test. The browser frontend (worker + OPFS) can't
3
+ * run headless in `bun test`, so this proves the piece that CAN rot silently:
4
+ * the server config + generated schema this scaffold ships actually sync. It
5
+ * boots the same server core config as `src/server.ts` on an ephemeral port
6
+ * and drives two bun:sqlite client cores through real HTTP to convergence.
7
+ *
8
+ * The frontend itself is covered by `tsc` (typecheck) and by the demo app /
9
+ * conformance suite in the syncular tree, which exercise the identical
10
+ * worker + OPFS + realtime path.
11
+ */
12
+ import { afterAll, beforeAll, expect, test } from 'bun:test';
13
+ import {
14
+ httpSegmentDownloader,
15
+ httpSyncTransport,
16
+ SyncClient,
17
+ } from '@syncular/client';
18
+ import { openBunDatabase } from '@syncular/client/bun';
19
+ import {
20
+ MemorySegmentStore,
21
+ SqliteServerStorage,
22
+ type SyncServerConfig,
23
+ } from '@syncular/server';
24
+ import { createSyncularHono } from '@syncular/server-hono';
25
+ import { schema, todoListSubscription } from './syncular.generated';
26
+
27
+ let server: ReturnType<typeof Bun.serve>;
28
+ let baseUrl: string;
29
+
30
+ beforeAll(() => {
31
+ const config: SyncServerConfig = {
32
+ schema,
33
+ storage: new SqliteServerStorage(':memory:'),
34
+ segments: new MemorySegmentStore(),
35
+ resolveScopes: () => ({ list_id: ['*'] }),
36
+ };
37
+ const app = createSyncularHono({
38
+ config,
39
+ authenticate: async () => ({ actorId: 'demo-user', partition: 'demo' }),
40
+ });
41
+ server = Bun.serve({ port: 0, fetch: app.fetch });
42
+ baseUrl = `http://localhost:${server.port}`;
43
+ });
44
+
45
+ afterAll(() => {
46
+ server.stop(true);
47
+ });
48
+
49
+ function makeClient(clientId: string): SyncClient {
50
+ return new SyncClient({
51
+ database: openBunDatabase(),
52
+ schema,
53
+ clientId,
54
+ transport: httpSyncTransport(`${baseUrl}/sync`),
55
+ segments: httpSegmentDownloader(`${baseUrl}/segments`),
56
+ });
57
+ }
58
+
59
+ test('two clients converge through the scaffolded server config', async () => {
60
+ const a = makeClient('client-a');
61
+ const b = makeClient('client-b');
62
+ await a.start();
63
+ await b.start();
64
+
65
+ const scopes = todoListSubscription.scopes({ listId: 'welcome' });
66
+ a.subscribe({ id: 'todos', table: 'todos', scopes });
67
+ b.subscribe({ id: 'todos', table: 'todos', scopes });
68
+
69
+ a.mutate([
70
+ {
71
+ table: 'todos',
72
+ op: 'upsert',
73
+ values: {
74
+ id: 'todo-1',
75
+ list_id: 'welcome',
76
+ title: 'Buy milk',
77
+ done: false,
78
+ position: 1,
79
+ updated_at_ms: Date.now(),
80
+ },
81
+ },
82
+ ]);
83
+ await a.syncUntilIdle();
84
+ await b.syncUntilIdle();
85
+
86
+ const rows = b.query('SELECT id, title, done FROM todos ORDER BY id');
87
+ expect(rows).toEqual([{ id: 'todo-1', title: 'Buy milk', done: 0 }]);
88
+
89
+ await a.close();
90
+ await b.close();
91
+ });
@@ -0,0 +1,74 @@
1
+ // Generated by @syncular/typegen — DO NOT EDIT.
2
+ // irVersion: 1
3
+ // irHash: sha256:2ffc5b70cb96cd546326de9c06868bd988c9fa3459fb3bac8428a81dcc85fca0
4
+
5
+ /** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
6
+ export const schema = {
7
+ version: 1,
8
+ tables: [
9
+ {
10
+ name: 'todos',
11
+ columns: [
12
+ { name: 'id', type: 'string', nullable: false },
13
+ { name: 'list_id', type: 'string', nullable: false },
14
+ { name: 'title', type: 'string', nullable: false },
15
+ { name: 'done', type: 'boolean', nullable: false },
16
+ { name: 'position', type: 'integer', nullable: false },
17
+ { name: 'updated_at_ms', type: 'integer', nullable: false },
18
+ ],
19
+ primaryKey: 'id',
20
+ scopes: [
21
+ { pattern: 'list:{list_id}', column: 'list_id' },
22
+ ],
23
+ },
24
+ ],
25
+ } as const;
26
+
27
+ /** One todos row (§2.4 column order). */
28
+ export interface TodosRow {
29
+ id: string;
30
+ list_id: string;
31
+ title: string;
32
+ done: boolean;
33
+ position: number;
34
+ updated_at_ms: number;
35
+ }
36
+
37
+ /** Insert shape: nullable columns may be omitted. */
38
+ export interface TodosInsert {
39
+ id: string;
40
+ list_id: string;
41
+ title: string;
42
+ done: boolean;
43
+ position: number;
44
+ updated_at_ms: number;
45
+ }
46
+
47
+ /** Update shape: primary key required, all other columns optional. */
48
+ export interface TodosUpdate {
49
+ id: string;
50
+ list_id?: string;
51
+ title?: string;
52
+ done?: boolean;
53
+ position?: number;
54
+ updated_at_ms?: number;
55
+ }
56
+
57
+ /** Kysely `Database` interface (table → Row); the generic for
58
+ * @syncular/kysely's SyncularDialect. */
59
+ export interface Database {
60
+ todos: TodosRow;
61
+ }
62
+
63
+ export interface TodoListParams {
64
+ listId: string;
65
+ }
66
+
67
+ /** Requested-scope template for the 'todoList' subscription. */
68
+ export const todoListSubscription = {
69
+ name: 'todoList',
70
+ table: 'todos',
71
+ scopes: (params: TodoListParams): Record<string, string[]> => ({
72
+ list_id: [params.listId],
73
+ }),
74
+ } as const;
@@ -0,0 +1,76 @@
1
+ {
2
+ "irVersion": 1,
3
+ "schemaVersion": 1,
4
+ "schemaVersions": [
5
+ {
6
+ "version": 1,
7
+ "migrations": [
8
+ "0001_initial"
9
+ ]
10
+ }
11
+ ],
12
+ "tables": [
13
+ {
14
+ "name": "todos",
15
+ "primaryKey": "id",
16
+ "columns": [
17
+ {
18
+ "name": "id",
19
+ "type": "string",
20
+ "nullable": false
21
+ },
22
+ {
23
+ "name": "list_id",
24
+ "type": "string",
25
+ "nullable": false
26
+ },
27
+ {
28
+ "name": "title",
29
+ "type": "string",
30
+ "nullable": false
31
+ },
32
+ {
33
+ "name": "done",
34
+ "type": "boolean",
35
+ "nullable": false
36
+ },
37
+ {
38
+ "name": "position",
39
+ "type": "integer",
40
+ "nullable": false
41
+ },
42
+ {
43
+ "name": "updated_at_ms",
44
+ "type": "integer",
45
+ "nullable": false
46
+ }
47
+ ],
48
+ "scopes": [
49
+ {
50
+ "pattern": "list:{list_id}",
51
+ "variable": "list_id",
52
+ "column": "list_id"
53
+ }
54
+ ],
55
+ "extensions": {}
56
+ }
57
+ ],
58
+ "subscriptions": [
59
+ {
60
+ "name": "todoList",
61
+ "table": "todos",
62
+ "scopes": [
63
+ {
64
+ "variable": "list_id",
65
+ "values": [
66
+ {
67
+ "kind": "parameter",
68
+ "name": "listId"
69
+ }
70
+ ]
71
+ }
72
+ ]
73
+ }
74
+ ],
75
+ "extensions": {}
76
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "manifestVersion": 1,
3
+ "migrations": "./migrations",
4
+ "output": {
5
+ "ir": "./syncular.ir.json",
6
+ "module": "./src/syncular.generated.ts"
7
+ },
8
+ "schemaVersions": [{ "version": 1, "through": "0001_initial" }],
9
+ "tables": [{ "name": "todos", "scopes": ["list:{list_id}"] }],
10
+ "subscriptions": [
11
+ {
12
+ "name": "todoList",
13
+ "table": "todos",
14
+ "scopes": { "list_id": ["{listId}"] }
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "types": ["bun"],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "verbatimModuleSyntax": true,
11
+ "isolatedModules": true,
12
+ "skipLibCheck": true,
13
+ "noEmit": true
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }