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
@@ -1,769 +0,0 @@
1
- // @generated by `cargo run --manifest-path rust/Cargo.toml -p syncular-codegen --`
2
- // Source: migrations/*.sql and generated Syncular codegen handoff
3
-
4
- import { SYNCULAR_PACKAGE_NAME, SYNCULAR_PACKAGE_VERSION, SYNCULAR_WORKER_PROTOCOL_VERSION, createSyncularCommandHistory, createSyncularDatabase, withSyncularSchemaWrites } from '@syncular/client';
5
- import type { CreateSyncularDatabaseOptions, SyncularAppSchema, SyncularChangedCrdtField, SyncularChangedRow, SyncularCommandHistory, SyncularDatabase, SyncularEmbeddedMigration, SyncularFieldEncryptionConfig, SyncularFieldEncryptionRule, SyncularRowsChangedEvent, SyncularRuntimeInfo, SyncularYjsPayloadEnvelope } from '@syncular/client';
6
-
7
- import { sql, type Generated, type Kysely } from 'kysely';
8
- import { codecs, type BlobRef, type ColumnCodecSource } from '@syncular/core';
9
-
10
- export interface SyncularGeneratedOperation {
11
- table: string;
12
- row_id: string;
13
- op: 'upsert' | 'delete';
14
- payload: Record<string, unknown> | null;
15
- base_version?: number | null;
16
- }
17
-
18
- export interface SyncularSubscriptionSpec {
19
- id: string;
20
- table: string;
21
- scopes: Record<string, string | string[]>;
22
- params: Record<string, unknown>;
23
- bootstrapPhase?: number;
24
- }
25
-
26
- export interface SyncularSubscriptionArgs {
27
- actorId: string;
28
- projectId?: string | null;
29
- bootstrapPhases?: Record<string, number>;
30
- }
31
-
32
- function syncularBootstrapPhase(args: SyncularSubscriptionArgs, table: string, subscriptionId: string): number {
33
- return args.bootstrapPhases?.[subscriptionId] ?? args.bootstrapPhases?.[table] ?? 0;
34
- }
35
-
36
- export interface SyncularAppDb {
37
- tasks: TaskTable;
38
- }
39
-
40
- export interface SyncularGeneratedTableConfig {
41
- primaryKeyColumn: string;
42
- serverVersionColumn: string;
43
- softDeleteColumn: string | null;
44
- subscriptionId: string;
45
- subscriptionParams: Record<string, unknown>;
46
- scopeColumns: Record<string, string>;
47
- blobColumns: readonly string[];
48
- crdtYjsFields: readonly { field: string; stateColumn: string; containerKey: string; rowIdField: string; kind: 'text' | 'xml-fragment' | 'prosemirror'; syncMode: 'server-merge' | 'encrypted-update-log' }[];
49
- encryptedFields: readonly { field: string; scope: string; rowIdField: string }[];
50
- }
51
-
52
- export const syncularGeneratedSchemaVersion = 1 as const;
53
- export const syncularGeneratedClientSchemaSupport = {
54
- current: syncularGeneratedSchemaVersion,
55
- minSupported: 1,
56
- supported: [1],
57
- } as const;
58
- export type SyncularGeneratedSupportedClientSchemaVersion = typeof syncularGeneratedClientSchemaSupport.supported[number];
59
- export function syncularIsSupportedClientSchemaVersion(schemaVersion: number | null | undefined): schemaVersion is SyncularGeneratedSupportedClientSchemaVersion {
60
- if (typeof schemaVersion !== 'number') return false;
61
- return (syncularGeneratedClientSchemaSupport.supported as readonly number[]).includes(schemaVersion);
62
- }
63
-
64
- const syncularGeneratedSchemaId = 'syncular-app';
65
-
66
- export interface SyncularGeneratedAppMigration {
67
- version: string;
68
- schemaVersion: number;
69
- name: string;
70
- appSql: readonly string[];
71
- skippedSystemStatements: number;
72
- }
73
-
74
- export interface SyncularGeneratedEmbeddedMigration extends SyncularEmbeddedMigration {
75
- version: string;
76
- schemaVersion: number;
77
- name: string;
78
- upSql: string;
79
- }
80
-
81
- export const syncularGeneratedAppMigrations: readonly SyncularGeneratedAppMigration[] = [
82
- {
83
- version: '0001',
84
- schemaVersion: 1,
85
- name: 'initial',
86
- appSql: [
87
- `CREATE TABLE IF NOT EXISTS tasks (
88
- id TEXT PRIMARY KEY,
89
- title TEXT NOT NULL,
90
- completed INTEGER NOT NULL DEFAULT 0,
91
- user_id TEXT NOT NULL,
92
- created_at BIGINT NOT NULL DEFAULT 0,
93
- server_version BIGINT NOT NULL DEFAULT 0
94
- ) WITHOUT ROWID;`,
95
- ],
96
- skippedSystemStatements: 0,
97
- },
98
- ];
99
-
100
- export const syncularGeneratedEmbeddedMigrations: readonly SyncularGeneratedEmbeddedMigration[] = [
101
- {
102
- version: '0001',
103
- schemaVersion: 1,
104
- name: 'initial',
105
- upSql: 'CREATE TABLE IF NOT EXISTS tasks (\n id TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n completed INTEGER NOT NULL DEFAULT 0,\n user_id TEXT NOT NULL,\n created_at BIGINT NOT NULL DEFAULT 0,\n server_version BIGINT NOT NULL DEFAULT 0\n) WITHOUT ROWID;',
106
- },
107
- ];
108
-
109
- export const syncularGeneratedRequiredRuntimeFeatures = [
110
- 'web-owned-sqlite-core',
111
- ] as const;
112
-
113
- export const syncularGeneratedTableConfig = {
114
- tasks: {
115
- primaryKeyColumn: 'id',
116
- serverVersionColumn: 'server_version',
117
- softDeleteColumn: null,
118
- subscriptionId: 'sub-tasks',
119
- subscriptionParams: {},
120
- scopeColumns: {
121
- user_id: 'user_id',
122
- },
123
- blobColumns: [],
124
- crdtYjsFields: [],
125
- encryptedFields: [],
126
- },
127
- } satisfies Record<'tasks', SyncularGeneratedTableConfig>;
128
-
129
- export interface SyncularGeneratedLocalReadModel {
130
- name: string;
131
- outputTable: keyof SyncularAppDb;
132
- setupSql: readonly string[];
133
- rebuildSql: readonly string[];
134
- }
135
-
136
- export const syncularGeneratedLocalReadModels: readonly SyncularGeneratedLocalReadModel[] = [
137
- ];
138
-
139
- export interface SyncularGeneratedLocalIndex {
140
- table: keyof SyncularAppDb;
141
- name: string;
142
- sql: string;
143
- unique: boolean;
144
- partial: boolean;
145
- columns: readonly { name: string | null; descending: boolean }[];
146
- }
147
-
148
- export const syncularGeneratedLocalIndexes: readonly SyncularGeneratedLocalIndex[] = [
149
- ];
150
-
151
- export interface SyncularGeneratedDerivedSchemaTimings {
152
- totalMs: number;
153
- metadataMs: number;
154
- indexesMs: number;
155
- readModelProbeMs: number;
156
- readModelSetupMs: number;
157
- readModelRebuildMs: number;
158
- recordSchemaVersionMs: number;
159
- rebuiltReadModels: string[];
160
- }
161
-
162
- export interface SyncularGeneratedSchemaInstallTimings extends SyncularGeneratedDerivedSchemaTimings {
163
- baseSchemaMs: number;
164
- derivedSchemaMs: number;
165
- }
166
-
167
- export const syncularGeneratedAppSchema = {
168
- schemaVersion: syncularGeneratedSchemaVersion,
169
- migrations: syncularGeneratedEmbeddedMigrations,
170
- localBaseSchema: {
171
- tableSetupSql: [
172
- 'CREATE TABLE IF NOT EXISTS "tasks" (\n "id" TEXT PRIMARY KEY,\n "title" TEXT NOT NULL,\n "completed" INTEGER NOT NULL DEFAULT 0,\n "user_id" TEXT NOT NULL,\n "created_at" INTEGER NOT NULL DEFAULT 0,\n "server_version" INTEGER NOT NULL DEFAULT 0\n) WITHOUT ROWID',
173
- ],
174
- },
175
- tables: [
176
- {
177
- name: 'tasks',
178
- primaryKeyColumn: 'id',
179
- serverVersionColumn: 'server_version',
180
- softDeleteColumn: null,
181
- subscriptionId: 'sub-tasks',
182
- columns: [
183
- { name: 'id', typeFamily: 'text', notnullRequired: false, primaryKey: true },
184
- { name: 'title', typeFamily: 'text', notnullRequired: true, primaryKey: false },
185
- { name: 'completed', typeFamily: 'integer', notnullRequired: true, primaryKey: false },
186
- { name: 'user_id', typeFamily: 'text', notnullRequired: true, primaryKey: false },
187
- { name: 'created_at', typeFamily: 'integer', notnullRequired: true, primaryKey: false },
188
- { name: 'server_version', typeFamily: 'integer', notnullRequired: true, primaryKey: false },
189
- ],
190
- blobColumns: [],
191
- crdtYjsFields: [],
192
- encryptedFields: [],
193
- scopes: [{ name: 'user_id', column: 'user_id', source: 'actorId', required: true }],
194
- },
195
- ],
196
- } satisfies SyncularAppSchema;
197
-
198
- export const syncularGeneratedFieldEncryptionRules = [
199
- ] satisfies readonly SyncularFieldEncryptionRule[];
200
-
201
- export function syncularGeneratedFieldEncryptionConfig(
202
- options: Omit<SyncularFieldEncryptionConfig, 'rules'>
203
- ): SyncularFieldEncryptionConfig {
204
- return {
205
- ...options,
206
- rules: [...syncularGeneratedFieldEncryptionRules],
207
- };
208
- }
209
-
210
- const syncularGeneratedAppTableNames = [
211
- 'tasks',
212
- ] as const satisfies readonly (keyof SyncularAppDb)[];
213
-
214
- const syncularGeneratedAppTableRefs = {
215
- tasks: {
216
- name: 'tasks',
217
- config: syncularGeneratedTableConfig.tasks,
218
- },
219
- } as const;
220
-
221
- export type SyncularGeneratedChangedOperation = SyncularChangedRow['operation'];
222
- export type SyncularChangedRowsInput = SyncularRowsChangedEvent | { changedRows?: readonly SyncularChangedRow[] } | readonly SyncularChangedRow[];
223
-
224
- export interface SyncularGeneratedChangedRowBase<Table extends keyof SyncularAppDb, Field extends string> {
225
- raw: SyncularChangedRow;
226
- table: Table;
227
- rowId: string | null;
228
- operation: SyncularGeneratedChangedOperation;
229
- changedFields: Field[];
230
- crdtFields: Field[];
231
- crdtFieldChanges: (SyncularChangedCrdtField & { field: Field })[];
232
- changed: Record<Field, boolean>;
233
- crdt: Record<Field, boolean>;
234
- commitId: string | null;
235
- commitSeq: number | null;
236
- subscriptionId: string | null;
237
- serverVersion: number | null;
238
- isInsert: boolean;
239
- isUpdate: boolean;
240
- isDelete: boolean;
241
- }
242
-
243
- function syncularRowsFromChangedInput(input: SyncularChangedRowsInput): readonly SyncularChangedRow[] {
244
- return Array.isArray(input) ? input : (input as { changedRows?: readonly SyncularChangedRow[] }).changedRows ?? [];
245
- }
246
-
247
- function syncularColumnFlags<Field extends string>(fields: readonly string[], allFields: readonly Field[]): Record<Field, boolean> {
248
- const changed = new Set(fields);
249
- return Object.fromEntries(allFields.map((field) => [field, changed.has(field)])) as Record<Field, boolean>;
250
- }
251
-
252
- function syncularTypedChangedRows<Table extends keyof SyncularAppDb, Field extends string>(
253
- input: SyncularChangedRowsInput,
254
- table: Table,
255
- fields: readonly Field[]
256
- ): SyncularGeneratedChangedRowBase<Table, Field>[] {
257
- const fieldSet = new Set<string>(fields);
258
- return syncularRowsFromChangedInput(input)
259
- .filter((row) => row.table === table)
260
- .map((row) => ({
261
- raw: row,
262
- table,
263
- rowId: row.rowId ?? null,
264
- operation: row.operation,
265
- changedFields: row.changedFields.filter((field): field is Field => fieldSet.has(field)),
266
- crdtFields: row.crdtFields.filter((field): field is Field => fieldSet.has(field)),
267
- crdtFieldChanges: (row.crdtFieldChanges ?? []).filter((field): field is SyncularChangedCrdtField & { field: Field } => fieldSet.has(field.field)),
268
- changed: syncularColumnFlags(row.changedFields, fields),
269
- crdt: syncularColumnFlags(row.crdtFields, fields),
270
- commitId: row.commitId ?? null,
271
- commitSeq: row.commitSeq ?? null,
272
- subscriptionId: row.subscriptionId ?? null,
273
- serverVersion: row.serverVersion ?? null,
274
- isInsert: row.operation === 'insert',
275
- isUpdate: row.operation === 'update',
276
- isDelete: row.operation === 'delete',
277
- }));
278
- }
279
-
280
- export const syncularTaskChangedFields = [
281
- 'id',
282
- 'title',
283
- 'completed',
284
- 'user_id',
285
- 'created_at',
286
- 'server_version',
287
- ] as const;
288
- export type TaskChangedField = typeof syncularTaskChangedFields[number];
289
- export type TaskChangedColumns = Record<TaskChangedField, boolean>;
290
- export type TaskChangedRow = SyncularGeneratedChangedRowBase<'tasks', TaskChangedField>;
291
- export function taskChangedRows(input: SyncularChangedRowsInput): TaskChangedRow[] {
292
- return syncularTypedChangedRows(input, 'tasks', syncularTaskChangedFields);
293
- }
294
-
295
- export type SyncularAppChangedRow =
296
- TaskChangedRow;
297
-
298
- export function syncularAppChangedRows(input: SyncularChangedRowsInput): SyncularAppChangedRow[] {
299
- return [
300
- ...taskChangedRows(input),
301
- ];
302
- }
303
-
304
- export const syncularChangedRows = {
305
- tasks: taskChangedRows,
306
- } as const;
307
-
308
- export const syncularGeneratedCodecs: ColumnCodecSource = (column) => {
309
- const table = syncularGeneratedTableConfig[column.table as keyof typeof syncularGeneratedTableConfig];
310
- const blobColumns: readonly string[] = table?.blobColumns ?? [];
311
- if (blobColumns.includes(column.column)) {
312
- return codecs.stringJson<BlobRef>({ ts: { type: 'BlobRef', import: { name: 'BlobRef', from: '@syncular/core' } } });
313
- }
314
- return undefined;
315
- };
316
-
317
- function withSyncularGeneratedCodecs(userCodecs?: ColumnCodecSource): ColumnCodecSource {
318
- return (column) => syncularGeneratedCodecs(column) ?? userCodecs?.(column);
319
- }
320
-
321
- export const syncularGeneratedApp = {
322
- currentSchemaVersion: syncularGeneratedSchemaVersion,
323
- appSchema: syncularGeneratedAppSchema,
324
- tables: syncularGeneratedAppTableRefs,
325
- tableNames: syncularGeneratedAppTableNames,
326
- tableConfig: syncularGeneratedTableConfig,
327
- fieldEncryptionRules: syncularGeneratedFieldEncryptionRules,
328
- fieldEncryptionConfig: syncularGeneratedFieldEncryptionConfig,
329
- codecs: syncularGeneratedCodecs,
330
- } as const;
331
-
332
- export async function ensureSyncularAppBaseSchema(db: Kysely<any>): Promise<void> {
333
- await ensureSyncularAppBaseSchemaWithMetadata(db);
334
- }
335
-
336
- async function ensureSyncularAppBaseSchemaWithMetadata(db: Kysely<any>): Promise<number | null> {
337
- const syncularGeneratedPreviousSchemaVersion = await ensureSyncularAppSchemaMetadata(db);
338
- for (const statement of syncularGeneratedAppSchema.localBaseSchema.tableSetupSql) {
339
- await sql.raw(statement).execute(db);
340
- }
341
- return syncularGeneratedPreviousSchemaVersion;
342
- }
343
-
344
- function syncularGeneratedNowMs(): number {
345
- return typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
346
- }
347
-
348
- export async function ensureSyncularAppDerivedSchemaWithTimings(db: Kysely<any>, syncularGeneratedKnownSchemaVersion?: number | null): Promise<SyncularGeneratedDerivedSchemaTimings> {
349
- const syncularGeneratedTotalStartedAt = syncularGeneratedNowMs();
350
- const syncularGeneratedTimings: SyncularGeneratedDerivedSchemaTimings = {
351
- totalMs: 0,
352
- metadataMs: 0,
353
- indexesMs: 0,
354
- readModelProbeMs: 0,
355
- readModelSetupMs: 0,
356
- readModelRebuildMs: 0,
357
- recordSchemaVersionMs: 0,
358
- rebuiltReadModels: [],
359
- };
360
- let syncularGeneratedStartedAt = syncularGeneratedNowMs();
361
- if (syncularGeneratedKnownSchemaVersion === undefined) {
362
- await ensureSyncularAppSchemaMetadata(db);
363
- }
364
- syncularGeneratedTimings.metadataMs = syncularGeneratedNowMs() - syncularGeneratedStartedAt;
365
- syncularGeneratedStartedAt = syncularGeneratedNowMs();
366
- await ensureSyncularAppIndexes(db);
367
- syncularGeneratedTimings.indexesMs = syncularGeneratedNowMs() - syncularGeneratedStartedAt;
368
- syncularGeneratedStartedAt = syncularGeneratedNowMs();
369
- await recordSyncularAppSchemaVersion(db);
370
- syncularGeneratedTimings.recordSchemaVersionMs = syncularGeneratedNowMs() - syncularGeneratedStartedAt;
371
- syncularGeneratedTimings.totalMs = syncularGeneratedNowMs() - syncularGeneratedTotalStartedAt;
372
- return syncularGeneratedTimings;
373
- }
374
-
375
- export async function ensureSyncularAppDerivedSchema(db: Kysely<any>): Promise<void> {
376
- await ensureSyncularAppDerivedSchemaWithTimings(db);
377
- }
378
-
379
- export async function ensureSyncularAppIndexes(db: Kysely<any>): Promise<void> {
380
- for (const index of syncularGeneratedLocalIndexes) {
381
- await sql.raw(index.sql).execute(db);
382
- }
383
- }
384
-
385
- export async function ensureSyncularAppReadModelSetup(db: Kysely<any>): Promise<void> {
386
- for (const readModel of syncularGeneratedLocalReadModels) {
387
- for (const statement of readModel.setupSql) {
388
- await sql.raw(statement).execute(db);
389
- }
390
- }
391
- }
392
-
393
- export async function rebuildSyncularAppReadModels(db: Kysely<any>): Promise<void> {
394
- for (const readModel of syncularGeneratedLocalReadModels) {
395
- for (const statement of readModel.rebuildSql) {
396
- await sql.raw(statement).execute(db);
397
- }
398
- }
399
- }
400
-
401
- async function recordSyncularAppSchemaVersion(db: Kysely<any>): Promise<void> {
402
- await validateSyncularAppSchema(db);
403
- await sql`
404
- insert into syncular_app_schema (schema_id, schema_version, updated_at)
405
- values (${sql.val(syncularGeneratedSchemaId)}, ${sql.val(syncularGeneratedSchemaVersion)}, ${sql.val(Date.now())})
406
- on conflict (schema_id) do update set
407
- schema_version = excluded.schema_version,
408
- updated_at = excluded.updated_at
409
- `.execute(db);
410
- }
411
-
412
- export async function ensureSyncularAppSchema(db: Kysely<any>): Promise<void> {
413
- await ensureSyncularAppSchemaWithTimings(db);
414
- }
415
-
416
- export async function ensureSyncularAppSchemaWithTimings(db: Kysely<any>): Promise<SyncularGeneratedSchemaInstallTimings> {
417
- const syncularGeneratedTotalStartedAt = syncularGeneratedNowMs();
418
- const syncularGeneratedBaseStartedAt = syncularGeneratedNowMs();
419
- const syncularGeneratedPreviousSchemaVersion = await ensureSyncularAppBaseSchemaWithMetadata(db);
420
- const syncularGeneratedBaseSchemaMs = syncularGeneratedNowMs() - syncularGeneratedBaseStartedAt;
421
- const syncularGeneratedDerivedStartedAt = syncularGeneratedNowMs();
422
- const syncularGeneratedDerivedTimings = await ensureSyncularAppDerivedSchemaWithTimings(db, syncularGeneratedPreviousSchemaVersion);
423
- const syncularGeneratedDerivedSchemaMs = syncularGeneratedNowMs() - syncularGeneratedDerivedStartedAt;
424
- return {
425
- ...syncularGeneratedDerivedTimings,
426
- totalMs: syncularGeneratedNowMs() - syncularGeneratedTotalStartedAt,
427
- baseSchemaMs: syncularGeneratedBaseSchemaMs,
428
- derivedSchemaMs: syncularGeneratedDerivedSchemaMs,
429
- };
430
- }
431
-
432
- interface SyncularGeneratedColumnInfo {
433
- name: string;
434
- type: string;
435
- notnull: number;
436
- pk: number;
437
- }
438
-
439
- async function ensureSyncularAppSchemaMetadata(db: Kysely<any>): Promise<number | null> {
440
- await db.schema
441
- .createTable('syncular_app_schema')
442
- .ifNotExists()
443
- .addColumn('schema_id', 'text', (col) => col.primaryKey())
444
- .addColumn('schema_version', 'integer', (col) => col.notNull())
445
- .addColumn('updated_at', 'bigint', (col) => col.notNull())
446
- .execute();
447
-
448
- const rows = await sql<{ schema_version: number }>`
449
- select schema_version
450
- from syncular_app_schema
451
- where schema_id = ${sql.val(syncularGeneratedSchemaId)}
452
- limit 1
453
- `.execute(db);
454
- const version = rows.rows[0]?.schema_version;
455
- if (version == null) return null;
456
- const localVersion = Number(version);
457
- if (localVersion > syncularGeneratedSchemaVersion) {
458
- throw new Error(`Syncular app schema version mismatch: local ${localVersion}, generated ${syncularGeneratedSchemaVersion}. Regenerate the client before opening this database.`);
459
- }
460
- if (localVersion < syncularGeneratedSchemaVersion) {
461
- await applySyncularGeneratedAppMigrations(db, localVersion);
462
- }
463
- return localVersion;
464
- }
465
-
466
- async function applySyncularGeneratedAppMigrations(db: Kysely<any>, localVersion: number): Promise<void> {
467
- for (const migration of syncularGeneratedAppMigrations) {
468
- if (migration.schemaVersion <= localVersion || migration.schemaVersion > syncularGeneratedSchemaVersion) continue;
469
- for (const statement of migration.appSql) {
470
- await sql.raw(statement).execute(db);
471
- }
472
- }
473
- }
474
-
475
- async function validateSyncularAppSchema(db: Kysely<any>): Promise<void> {
476
- await validateSyncularGeneratedTable(db, 'tasks', [
477
- { name: 'id', type: 'text', notnull: 0, pk: 1 },
478
- { name: 'title', type: 'text', notnull: 1, pk: 0 },
479
- { name: 'completed', type: 'integer', notnull: 1, pk: 0 },
480
- { name: 'user_id', type: 'text', notnull: 1, pk: 0 },
481
- { name: 'created_at', type: 'integer', notnull: 1, pk: 0 },
482
- { name: 'server_version', type: 'integer', notnull: 1, pk: 0 },
483
- ]);
484
- }
485
-
486
- async function validateSyncularGeneratedTable(db: Kysely<any>, table: string, expected: SyncularGeneratedColumnInfo[]): Promise<void> {
487
- const rows = await sql<SyncularGeneratedColumnInfo>`
488
- select name, type, "notnull" as "notnull", pk
489
- from pragma_table_info(${sql.val(table)})
490
- `.execute(db);
491
- const actual = new Map(rows.rows.map((row) => [row.name, row]));
492
- for (const column of expected) {
493
- const found = actual.get(column.name);
494
- if (!found) throw new Error(`Syncular app schema mismatch: ${table}.${column.name} is missing`);
495
- if (sqliteTypeFamily(found.type) !== column.type) {
496
- throw new Error(`Syncular app schema mismatch: ${table}.${column.name} has type ${found.type}, expected ${column.type}`);
497
- }
498
- if (column.pk > 0 && Number(found.pk) <= 0) {
499
- throw new Error(`Syncular app schema mismatch: ${table}.${column.name} is not a primary key`);
500
- }
501
- if (column.notnull > 0 && Number(found.notnull) <= 0 && Number(found.pk) <= 0) {
502
- throw new Error(`Syncular app schema mismatch: ${table}.${column.name} is nullable`);
503
- }
504
- }
505
- }
506
-
507
- function sqliteTypeFamily(type: string | null | undefined): string {
508
- const upper = String(type ?? '').toUpperCase();
509
- if (upper.includes('INT')) return 'integer';
510
- if (upper.includes('REAL') || upper.includes('FLOA') || upper.includes('DOUB')) return 'real';
511
- if (upper.includes('BLOB')) return 'blob';
512
- return 'text';
513
- }
514
-
515
- export interface SyncularGeneratedMutationOptions {
516
- baseVersion?: number | null;
517
- }
518
-
519
- export interface SyncularGeneratedMutationReceipt {
520
- commitId: string;
521
- clientCommitId: string;
522
- }
523
-
524
- export type SyncularGeneratedInsertReceipt = SyncularGeneratedMutationReceipt & { id: string };
525
- export type SyncularGeneratedInsertManyReceipt = SyncularGeneratedMutationReceipt & { ids: string[] };
526
-
527
- export interface SyncularGeneratedTableMutations<NewRow, Patch> {
528
- insert(input: NewRow): Promise<SyncularGeneratedInsertReceipt>;
529
- insertMany(rows: readonly NewRow[]): Promise<SyncularGeneratedInsertManyReceipt>;
530
- update(rowId: string, patch: Patch, options?: SyncularGeneratedMutationOptions): Promise<SyncularGeneratedMutationReceipt>;
531
- delete(rowId: string, options?: SyncularGeneratedMutationOptions): Promise<SyncularGeneratedMutationReceipt>;
532
- upsert(rowId: string, patch: Patch, options?: SyncularGeneratedMutationOptions): Promise<SyncularGeneratedMutationReceipt>;
533
- }
534
-
535
- export interface SyncularGeneratedTableMutationsTx<NewRow, Patch> {
536
- insert(input: NewRow): Promise<string>;
537
- insertMany(rows: readonly NewRow[]): Promise<string[]>;
538
- update(rowId: string, patch: Patch, options?: SyncularGeneratedMutationOptions): Promise<void>;
539
- delete(rowId: string, options?: SyncularGeneratedMutationOptions): Promise<void>;
540
- upsert(rowId: string, patch: Patch, options?: SyncularGeneratedMutationOptions): Promise<void>;
541
- }
542
-
543
- export interface SyncularAppMutationsTx {
544
- tasks: SyncularGeneratedTableMutationsTx<NewTask, TaskPatch>;
545
- }
546
-
547
- export interface SyncularAppMutations {
548
- $commit<R>(fn: (tx: SyncularAppMutationsTx) => Promise<R> | R): Promise<{ result: R; commit: SyncularGeneratedMutationReceipt }>;
549
- tasks: SyncularGeneratedTableMutations<NewTask, TaskPatch>;
550
- }
551
-
552
- export type SyncularAppDatabase = Omit<SyncularDatabase<SyncularAppDb>, 'mutations' | 'leasedMutations'> & {
553
- mutations: SyncularAppMutations;
554
- leasedMutations: SyncularAppMutations;
555
- commandHistory: SyncularCommandHistory;
556
- };
557
- export type SyncularAppSubscriptionsOption =
558
- | false
559
- | readonly SyncularSubscriptionSpec[]
560
- | ((args: SyncularSubscriptionArgs) => readonly SyncularSubscriptionSpec[]);
561
-
562
- export interface CreateSyncularAppDatabaseOptions extends Omit<CreateSyncularDatabaseOptions, 'subscriptions'> {
563
- subscriptions?: SyncularAppSubscriptionsOption;
564
- bootstrapPhases?: Record<string, number>;
565
- schemaInstallMode?: 'derived' | 'full' | 'base' | 'none';
566
- }
567
-
568
- export async function assertSyncularAppRuntime(database: Pick<SyncularAppDatabase, 'client'>): Promise<void> {
569
- assertSyncularAppRuntimeInfo(await database.client.runtimeInfo());
570
- const schemaState = await database.client.generatedSchemaState();
571
- if (schemaState.currentSchemaVersion !== syncularGeneratedSchemaVersion) {
572
- throw new Error(`Syncular Rust app schema version mismatch: ${schemaState.currentSchemaVersion}, expected ${syncularGeneratedSchemaVersion}`);
573
- }
574
- if (schemaState.schemaVersion !== null && schemaState.schemaVersion > syncularGeneratedSchemaVersion) {
575
- throw new Error(`Syncular Rust local app schema version mismatch: local ${schemaState.schemaVersion}, generated ${syncularGeneratedSchemaVersion}`);
576
- }
577
- }
578
-
579
- export function assertSyncularAppRuntimeInfo(runtimeInfo: SyncularRuntimeInfo): void {
580
- if (runtimeInfo.packageName !== SYNCULAR_PACKAGE_NAME) {
581
- throw new Error(`Syncular runtime package mismatch: ${runtimeInfo.packageName}, expected ${SYNCULAR_PACKAGE_NAME}`);
582
- }
583
- if (runtimeInfo.packageVersion !== SYNCULAR_PACKAGE_VERSION) {
584
- throw new Error(`Syncular runtime package version mismatch: ${runtimeInfo.packageVersion}, expected ${SYNCULAR_PACKAGE_VERSION}`);
585
- }
586
- if (runtimeInfo.workerProtocolVersion !== SYNCULAR_WORKER_PROTOCOL_VERSION) {
587
- throw new Error(`Syncular worker protocol mismatch: ${runtimeInfo.workerProtocolVersion}, expected ${SYNCULAR_WORKER_PROTOCOL_VERSION}`);
588
- }
589
- if (!runtimeInfo.rust) {
590
- throw new Error('Syncular runtime did not report Rust runtime information');
591
- }
592
- for (const feature of syncularGeneratedRequiredRuntimeFeatures) {
593
- if (!runtimeInfo.rust.features.includes(feature)) {
594
- throw new Error(`Syncular Rust runtime is missing ${feature} support`);
595
- }
596
- }
597
- }
598
-
599
- function resolveSyncularAppSubscriptions(options: CreateSyncularAppDatabaseOptions): readonly SyncularSubscriptionSpec[] {
600
- const args: SyncularSubscriptionArgs = {
601
- actorId: options.config.actorId,
602
- projectId: options.config.projectId,
603
- bootstrapPhases: options.bootstrapPhases,
604
- };
605
- const subscriptions = options.subscriptions;
606
- if (subscriptions === false) return [];
607
- if (typeof subscriptions === 'function') return subscriptions(args);
608
- return subscriptions ?? defaultSyncularSubscriptions(args);
609
- }
610
-
611
- export async function finalizeSyncularAppDatabaseSchema(database: Pick<SyncularAppDatabase, 'client'>): Promise<void> {
612
- await withSyncularSchemaWrites(database, ensureSyncularAppDerivedSchema);
613
- }
614
-
615
- export async function createSyncularAppDatabase(
616
- options: CreateSyncularAppDatabaseOptions
617
- ): Promise<SyncularAppDatabase> {
618
- const { subscriptions: _appSubscriptions, ...databaseOptions } = options;
619
- const database = await createSyncularDatabase<SyncularAppDb>({
620
- ...databaseOptions,
621
- // The app factory installs the generated schema first; the lifecycle
622
- // starts below once subscriptions and command history are wired.
623
- lifecycle: { ...options.lifecycle, autoStart: false },
624
- config: {
625
- ...options.config,
626
- schemaVersion: options.config.schemaVersion ?? syncularGeneratedSchemaVersion,
627
- appSchema: options.config.appSchema ?? syncularGeneratedAppSchema,
628
- },
629
- codecs: withSyncularGeneratedCodecs(options.codecs),
630
- appTables: syncularGeneratedApp.tableNames,
631
- tableConfig: { ...options.tableConfig, ...syncularGeneratedTableConfig },
632
- requiredRuntimeFeatures: syncularGeneratedRequiredRuntimeFeatures,
633
- });
634
- try {
635
- await assertSyncularAppRuntime(database);
636
- const schemaInstallMode = options.schemaInstallMode ?? 'derived';
637
- if (schemaInstallMode === 'derived') {
638
- await withSyncularSchemaWrites(database, ensureSyncularAppDerivedSchema);
639
- } else if (schemaInstallMode === 'full') {
640
- await withSyncularSchemaWrites(database, ensureSyncularAppSchema);
641
- } else if (schemaInstallMode === 'base') {
642
- await withSyncularSchemaWrites(database, ensureSyncularAppBaseSchema);
643
- } else if (schemaInstallMode !== 'none') {
644
- throw new Error(`Unknown Syncular schemaInstallMode: ${schemaInstallMode}`);
645
- }
646
- await database.client.setSubscriptions(resolveSyncularAppSubscriptions(options));
647
- const appDatabase = database as unknown as SyncularAppDatabase;
648
- const commandHistory = createSyncularCommandHistory<SyncularAppDb>({
649
- client: database.client,
650
- tableConfig: syncularGeneratedTableConfig,
651
- mutations: database.mutations,
652
- leasedMutations: database.leasedMutations,
653
- });
654
- appDatabase.commandHistory = commandHistory.history;
655
- appDatabase.mutations = commandHistory.wrapMutations(database.mutations, 'mutations') as SyncularAppMutations;
656
- appDatabase.leasedMutations = commandHistory.wrapMutations(database.leasedMutations, 'leasedMutations') as SyncularAppMutations;
657
- if (options.lifecycle?.autoStart ?? (options.config.mode ?? 'remote') === 'remote') {
658
- await database.start();
659
- }
660
- return appDatabase;
661
- } catch (err) {
662
- await database.close();
663
- throw err;
664
- }
665
- }
666
-
667
- export function defaultSyncularSubscriptions(args: SyncularSubscriptionArgs): SyncularSubscriptionSpec[] {
668
- return [
669
- taskSubscription(args),
670
- ];
671
- }
672
-
673
- export interface TaskTable {
674
- id: string;
675
- title: string;
676
- completed: Generated<number>;
677
- user_id: string;
678
- created_at: Generated<number>;
679
- server_version: Generated<number>;
680
- }
681
-
682
- export interface TaskRow {
683
- id: string;
684
- title: string;
685
- completed: number;
686
- user_id: string;
687
- created_at: number;
688
- server_version: number;
689
- }
690
-
691
- export interface NewTask {
692
- id: string;
693
- title: string;
694
- completed?: number;
695
- user_id: string;
696
- created_at?: number;
697
- }
698
-
699
- export interface NewTaskPayload {
700
- title: string;
701
- completed: number;
702
- user_id: string;
703
- created_at: number;
704
- }
705
-
706
- export type TaskPatch = Partial<Pick<TaskRow, 'title' | 'completed' | 'user_id' | 'created_at'>>;
707
-
708
- export function newTaskPayload(input: NewTask): NewTaskPayload {
709
- const payload: Partial<NewTaskPayload> = {
710
- title: input.title,
711
- user_id: input.user_id,
712
- };
713
- payload.completed = input.completed ?? 0;
714
- payload.created_at = input.created_at ?? 0;
715
- return payload as NewTaskPayload;
716
- }
717
-
718
- export function taskPatchPayload(patch: TaskPatch): TaskPatch {
719
- const payload: TaskPatch = {};
720
- if (patch.title !== undefined) payload.title = patch.title;
721
- if (patch.completed !== undefined) payload.completed = patch.completed;
722
- if (patch.user_id !== undefined) payload.user_id = patch.user_id;
723
- if (patch.created_at !== undefined) payload.created_at = patch.created_at;
724
- return payload;
725
- }
726
-
727
- export function newTaskOperation(input: NewTask, baseVersion: number | null = 0): SyncularGeneratedOperation {
728
- const payload = newTaskPayload(input);
729
- return {
730
- table: 'tasks',
731
- row_id: String(input.id),
732
- op: 'upsert',
733
- payload: payload as unknown as Record<string, unknown>,
734
- base_version: baseVersion,
735
- };
736
- }
737
-
738
- export function patchTaskOperation(rowId: string, patch: TaskPatch, baseVersion: number | null = null): SyncularGeneratedOperation {
739
- const payload = taskPatchPayload(patch);
740
- return {
741
- table: 'tasks',
742
- row_id: rowId,
743
- op: 'upsert',
744
- payload: payload as unknown as Record<string, unknown>,
745
- base_version: baseVersion,
746
- };
747
- }
748
-
749
- export function deleteTaskOperation(rowId: string, baseVersion: number | null = null): SyncularGeneratedOperation {
750
- return {
751
- table: 'tasks',
752
- row_id: rowId,
753
- op: 'delete',
754
- payload: null,
755
- base_version: baseVersion,
756
- };
757
- }
758
-
759
- export function taskSubscription(args: SyncularSubscriptionArgs): SyncularSubscriptionSpec {
760
- const scopes: Record<string, string | string[]> = {};
761
- scopes['user_id'] = args.actorId;
762
- return {
763
- id: 'sub-tasks',
764
- table: 'tasks',
765
- scopes,
766
- params: {},
767
- bootstrapPhase: syncularBootstrapPhase(args, 'tasks', 'sub-tasks'),
768
- };
769
- }