create-syncular-app 0.1.3 → 0.2.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.
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 +17 -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 +17 -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,512 +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 { BinarySnapshotTableWriter, createSyncularErrorResponse, type BinarySnapshotColumn, type BinarySnapshotRowsEncoder, type BlobRef } from '@syncular/core';
5
- import { SyncClientSchemaUnsupportedError, type ApplyOperationResult, type ScopeValues, type ServerApplyOperationContext, type ServerContext, type ServerSnapshotContext, type ServerTableHandler, type StoredScopes, type SyncCoreDb, type SyncOperation, type SyncServerAuth } from '@syncular/server';
6
- import type { Generated } from 'kysely';
7
-
8
- export const syncularGeneratedSchemaVersion = 1 as const;
9
- export const syncularGeneratedClientSchemaSupport = {
10
- current: syncularGeneratedSchemaVersion,
11
- minSupported: 1,
12
- supported: [1],
13
- } as const;
14
- export type SyncularGeneratedSupportedClientSchemaVersion = typeof syncularGeneratedClientSchemaSupport.supported[number];
15
- export function syncularIsSupportedClientSchemaVersion(schemaVersion: number | null | undefined): schemaVersion is SyncularGeneratedSupportedClientSchemaVersion {
16
- if (typeof schemaVersion !== 'number') return false;
17
- return (syncularGeneratedClientSchemaSupport.supported as readonly number[]).includes(schemaVersion);
18
- }
19
-
20
- export interface SyncularUnsupportedClientSchemaResultOptions {
21
- opIndex: number;
22
- schemaVersion: number | null | undefined;
23
- }
24
-
25
- export function syncularUnsupportedClientSchemaResult(options: SyncularUnsupportedClientSchemaResultOptions): ApplyOperationResult {
26
- const response = createSyncularErrorResponse('sync.client_schema_unsupported', {
27
- message: `Client schema version ${options.schemaVersion ?? 'unknown'} is not supported. Supported client schema versions: ${syncularGeneratedClientSchemaSupport.supported.join(', ')}.`,
28
- });
29
- return {
30
- result: {
31
- opIndex: options.opIndex,
32
- status: 'error',
33
- error: response.message ?? response.error,
34
- code: response.code,
35
- retriable: response.retryable,
36
- },
37
- emittedChanges: [],
38
- };
39
- }
40
-
41
- export interface SyncularGeneratedColumnSchemaMetadata {
42
- name: string;
43
- sqlType: string;
44
- typeFamily: string;
45
- appType: string;
46
- nullable: boolean;
47
- notnullRequired: boolean;
48
- primaryKey: boolean;
49
- hasDefault: boolean;
50
- defaultSql: string | null;
51
- serverVersion: boolean;
52
- softDelete: boolean;
53
- blobRef: boolean;
54
- scope: string | null;
55
- }
56
-
57
- export interface SyncularGeneratedTableSchemaMetadata {
58
- name: string;
59
- primaryKeyColumn: string;
60
- serverVersionColumn: string;
61
- softDeleteColumn: string | null;
62
- columns: readonly SyncularGeneratedColumnSchemaMetadata[];
63
- indexes?: readonly { name: string; sql: string; unique: boolean; partial: boolean; columns: readonly { name: string | null; descending: boolean }[] }[];
64
- blobColumns: readonly string[];
65
- crdtYjsFields: readonly { field: string; stateColumn: string; containerKey: string; rowIdField: string; kind: string; syncMode: string }[];
66
- encryptedFields: readonly { field: string; scope: string; rowIdField: string }[];
67
- scopes: readonly { name: string; column: string; source: string; required: boolean }[];
68
- subscription: { id: string; params: Record<string, unknown> };
69
- sqliteWithoutRowid?: boolean;
70
- }
71
-
72
- export interface SyncularGeneratedClientSchemaMetadata {
73
- schemaVersion: number;
74
- tables: readonly SyncularGeneratedTableSchemaMetadata[];
75
- localBaseSchema?: { tableSetupSql: readonly string[] };
76
- }
77
-
78
- export const syncularGeneratedCurrentClientSchema: SyncularGeneratedClientSchemaMetadata = {
79
- "schemaVersion": 1,
80
- "tables": [
81
- {
82
- "blobColumns": [],
83
- "columns": [
84
- {
85
- "appType": "string",
86
- "blobRef": false,
87
- "defaultSql": null,
88
- "hasDefault": false,
89
- "name": "id",
90
- "notnullRequired": false,
91
- "nullable": false,
92
- "primaryKey": true,
93
- "scope": null,
94
- "serverVersion": false,
95
- "softDelete": false,
96
- "sqlType": "TEXT",
97
- "typeFamily": "text"
98
- },
99
- {
100
- "appType": "string",
101
- "blobRef": false,
102
- "defaultSql": null,
103
- "hasDefault": false,
104
- "name": "title",
105
- "notnullRequired": true,
106
- "nullable": false,
107
- "primaryKey": false,
108
- "scope": null,
109
- "serverVersion": false,
110
- "softDelete": false,
111
- "sqlType": "TEXT",
112
- "typeFamily": "text"
113
- },
114
- {
115
- "appType": "integer",
116
- "blobRef": false,
117
- "defaultSql": "0",
118
- "hasDefault": true,
119
- "name": "completed",
120
- "notnullRequired": true,
121
- "nullable": false,
122
- "primaryKey": false,
123
- "scope": null,
124
- "serverVersion": false,
125
- "softDelete": false,
126
- "sqlType": "INTEGER",
127
- "typeFamily": "integer"
128
- },
129
- {
130
- "appType": "string",
131
- "blobRef": false,
132
- "defaultSql": null,
133
- "hasDefault": false,
134
- "name": "user_id",
135
- "notnullRequired": true,
136
- "nullable": false,
137
- "primaryKey": false,
138
- "scope": "user_id",
139
- "serverVersion": false,
140
- "softDelete": false,
141
- "sqlType": "TEXT",
142
- "typeFamily": "text"
143
- },
144
- {
145
- "appType": "integer",
146
- "blobRef": false,
147
- "defaultSql": "0",
148
- "hasDefault": true,
149
- "name": "created_at",
150
- "notnullRequired": true,
151
- "nullable": false,
152
- "primaryKey": false,
153
- "scope": null,
154
- "serverVersion": false,
155
- "softDelete": false,
156
- "sqlType": "BIGINT",
157
- "typeFamily": "integer"
158
- },
159
- {
160
- "appType": "integer",
161
- "blobRef": false,
162
- "defaultSql": "0",
163
- "hasDefault": true,
164
- "name": "server_version",
165
- "notnullRequired": true,
166
- "nullable": false,
167
- "primaryKey": false,
168
- "scope": null,
169
- "serverVersion": true,
170
- "softDelete": false,
171
- "sqlType": "BIGINT",
172
- "typeFamily": "integer"
173
- }
174
- ],
175
- "crdtYjsFields": [],
176
- "encryptedFields": [],
177
- "name": "tasks",
178
- "primaryKeyColumn": "id",
179
- "scopes": [
180
- {
181
- "column": "user_id",
182
- "name": "user_id",
183
- "required": true,
184
- "source": "actorId"
185
- }
186
- ],
187
- "serverVersionColumn": "server_version",
188
- "softDeleteColumn": null,
189
- "sqliteWithoutRowid": true,
190
- "subscription": {
191
- "id": "sub-tasks",
192
- "params": {}
193
- }
194
- }
195
- ]
196
- } as const;
197
-
198
- export const syncularGeneratedHistoricalClientSchemas: readonly SyncularGeneratedClientSchemaMetadata[] = [] as const;
199
- export type SyncularGeneratedClientSchema = SyncularGeneratedClientSchemaMetadata;
200
- export function syncularGeneratedClientSchemaForVersion(schemaVersion: number | null | undefined): SyncularGeneratedClientSchema | null {
201
- if (schemaVersion === syncularGeneratedSchemaVersion) return syncularGeneratedCurrentClientSchema;
202
- return syncularGeneratedHistoricalClientSchemas.find((schema) => schema.schemaVersion === schemaVersion) ?? null;
203
- }
204
-
205
- export interface SyncularGeneratedValidationError {
206
- path: string;
207
- message: string;
208
- }
209
-
210
- export type SyncularGeneratedValidationResult<T> = { ok: true; value: T } | { ok: false; errors: SyncularGeneratedValidationError[] };
211
-
212
- function syncularGeneratedIsRecord(value: unknown): value is Record<string, unknown> {
213
- return typeof value === 'object' && value !== null && !Array.isArray(value);
214
- }
215
-
216
- export function syncularGeneratedTableSchemaForVersion(table: string, schemaVersion: number | null | undefined): SyncularGeneratedTableSchemaMetadata | null {
217
- const schema = syncularGeneratedClientSchemaForVersion(schemaVersion);
218
- return schema?.tables.find((candidate) => candidate.name === table) ?? null;
219
- }
220
-
221
- export function syncularProjectGeneratedClientRowForVersion(table: string, row: Record<string, unknown>, schemaVersion: number | null | undefined): Record<string, unknown> {
222
- const tableSchema = syncularGeneratedTableSchemaForVersion(table, schemaVersion);
223
- if (!tableSchema) throw new Error(`Unknown table or unsupported client schema version ${schemaVersion ?? 'unknown'} for ${table}`);
224
- const projected: Record<string, unknown> = {};
225
- for (const column of tableSchema.columns) {
226
- if (column.name in row) projected[column.name] = row[column.name];
227
- }
228
- return projected;
229
- }
230
-
231
- export function syncularValidateGeneratedClientRow(table: string, row: unknown, schemaVersion: number | null | undefined): SyncularGeneratedValidationResult<Record<string, unknown>> {
232
- const tableSchema = syncularGeneratedTableSchemaForVersion(table, schemaVersion);
233
- if (!tableSchema) return { ok: false, errors: [{ path: table, message: `Unknown table or unsupported client schema version ${schemaVersion ?? 'unknown'}` }] };
234
- if (!syncularGeneratedIsRecord(row)) return { ok: false, errors: [{ path: table, message: 'Expected row object' }] };
235
- const errors: SyncularGeneratedValidationError[] = [];
236
- const columns = new Set(tableSchema.columns.map((column) => column.name));
237
- for (const key of Object.keys(row)) {
238
- if (!columns.has(key)) errors.push({ path: `${table}.${key}`, message: 'Unknown column' });
239
- }
240
- for (const column of tableSchema.columns) {
241
- if (!(column.name in row)) {
242
- errors.push({ path: `${table}.${column.name}`, message: 'Missing column' });
243
- continue;
244
- }
245
- if (row[column.name] == null && !column.nullable) {
246
- errors.push({ path: `${table}.${column.name}`, message: 'Column cannot be null' });
247
- }
248
- }
249
- return errors.length === 0 ? { ok: true, value: row } : { ok: false, errors };
250
- }
251
-
252
- export function syncularValidateGeneratedMutationPayload(table: string, payload: unknown, schemaVersion: number | null | undefined): SyncularGeneratedValidationResult<Record<string, unknown> | null> {
253
- const tableSchema = syncularGeneratedTableSchemaForVersion(table, schemaVersion);
254
- if (!tableSchema) return { ok: false, errors: [{ path: table, message: `Unknown table or unsupported client schema version ${schemaVersion ?? 'unknown'}` }] };
255
- if (payload == null) return { ok: true, value: null };
256
- if (!syncularGeneratedIsRecord(payload)) return { ok: false, errors: [{ path: table, message: 'Expected mutation payload object or null' }] };
257
- const errors: SyncularGeneratedValidationError[] = [];
258
- const columns = new Map(tableSchema.columns.map((column) => [column.name, column]));
259
- for (const [key, value] of Object.entries(payload)) {
260
- const column = columns.get(key);
261
- if (!column) {
262
- errors.push({ path: `${table}.${key}`, message: 'Unknown column' });
263
- continue;
264
- }
265
- if (value == null && !column.nullable) {
266
- errors.push({ path: `${table}.${key}`, message: 'Column cannot be null' });
267
- }
268
- }
269
- return errors.length === 0 ? { ok: true, value: payload } : { ok: false, errors };
270
- }
271
-
272
- export function syncularValidateGeneratedOperation(table: string, op: SyncOperation, schemaVersion: number | null | undefined): SyncularGeneratedValidationResult<SyncOperation> {
273
- if (op.table !== table) return { ok: false, errors: [{ path: op.table, message: `Expected operation table ${table}` }] };
274
- if (op.op === 'delete') {
275
- if (op.payload !== null) return { ok: false, errors: [{ path: `${table}.payload`, message: 'Delete payload must be null' }] };
276
- return { ok: true, value: op };
277
- }
278
- if (op.op !== 'upsert') return { ok: false, errors: [{ path: `${table}.op`, message: `Unsupported operation ${op.op}` }] };
279
- if (op.payload == null) return { ok: false, errors: [{ path: `${table}.payload`, message: 'Upsert payload is required' }] };
280
- const payload = syncularValidateGeneratedMutationPayload(table, op.payload, schemaVersion);
281
- return payload.ok ? { ok: true, value: op } : payload;
282
- }
283
-
284
- export interface SyncularAppDb {
285
- tasks: TaskTable;
286
- }
287
-
288
- export interface TaskTable {
289
- id: string;
290
- title: string;
291
- completed: Generated<number>;
292
- user_id: string;
293
- created_at: Generated<number>;
294
- server_version: Generated<number>;
295
- }
296
-
297
- export interface TaskRow {
298
- id: string;
299
- title: string;
300
- completed: number;
301
- user_id: string;
302
- created_at: number;
303
- server_version: number;
304
- }
305
-
306
- export const syncularGeneratedSnapshotBinaryColumns = {
307
- tasks: [
308
- { name: 'id', type: 'string' },
309
- { name: 'title', type: 'string' },
310
- { name: 'completed', type: 'integer' },
311
- { name: 'user_id', type: 'string' },
312
- { name: 'created_at', type: 'integer' },
313
- { name: 'server_version', type: 'integer' },
314
- ],
315
- } satisfies Record<keyof SyncularAppDb, readonly BinarySnapshotColumn[]>;
316
-
317
- export function encodeTasksBinarySnapshotRows(rows: readonly TaskRow[]): Uint8Array {
318
- const writer = new BinarySnapshotTableWriter('tasks', syncularGeneratedSnapshotBinaryColumns.tasks, rows.length);
319
- for (const row of rows) {
320
- writer.beginRow();
321
- writer.writeString(row.id, 'binary snapshot tasks.id');
322
- writer.writeString(row.title, 'binary snapshot tasks.title');
323
- writer.writeInteger(row.completed, 'binary snapshot tasks.completed');
324
- writer.writeString(row.user_id, 'binary snapshot tasks.user_id');
325
- writer.writeInteger(row.created_at, 'binary snapshot tasks.created_at');
326
- writer.writeInteger(row.server_version, 'binary snapshot tasks.server_version');
327
- }
328
- return writer.finish();
329
- }
330
-
331
- export const syncularGeneratedSnapshotBinaryEncoders = {
332
- tasks: (rows) => encodeTasksBinarySnapshotRows(rows as readonly TaskRow[]),
333
- } satisfies Record<keyof SyncularAppDb, BinarySnapshotRowsEncoder>;
334
-
335
- function syncularGeneratedSnapshotBinaryColumnType(column: SyncularGeneratedColumnSchemaMetadata): BinarySnapshotColumn['type'] {
336
- if (column.blobRef || column.appType === 'blobRef') return 'json';
337
- switch (column.typeFamily) {
338
- case 'integer': return 'integer';
339
- case 'real': return 'float';
340
- case 'blob': return 'bytes';
341
- default: return 'string';
342
- }
343
- }
344
-
345
- export function syncularGeneratedSnapshotBinaryColumnsForVersion(table: string, schemaVersion: number | null | undefined): readonly BinarySnapshotColumn[] | null {
346
- const tableSchema = syncularGeneratedTableSchemaForVersion(table, schemaVersion);
347
- if (!tableSchema) return null;
348
- return tableSchema.columns.map((column) => ({
349
- name: column.name,
350
- type: syncularGeneratedSnapshotBinaryColumnType(column),
351
- ...(column.nullable ? { nullable: true } : {}),
352
- }));
353
- }
354
-
355
- export function syncularGeneratedSnapshotBinaryEncoderForVersion(table: string, schemaVersion: number | null | undefined): BinarySnapshotRowsEncoder | null {
356
- if (schemaVersion !== syncularGeneratedSchemaVersion) return null;
357
- return syncularGeneratedSnapshotBinaryEncoders[table as keyof SyncularAppDb] ?? null;
358
- }
359
-
360
- export const syncularGeneratedServerSnapshotBinary = {
361
- columns: syncularGeneratedSnapshotBinaryColumns,
362
- encoders: syncularGeneratedSnapshotBinaryEncoders,
363
- columnsForVersion: syncularGeneratedSnapshotBinaryColumnsForVersion,
364
- encoderForVersion: syncularGeneratedSnapshotBinaryEncoderForVersion,
365
- } as const;
366
- const syncularGeneratedAppTableRefs = {
367
- tasks: {
368
- name: 'tasks',
369
- primaryKeyColumn: 'id',
370
- serverVersionColumn: 'server_version',
371
- scopePatterns: ['{user_id}'],
372
- },
373
- } as const;
374
-
375
- export const syncularGeneratedApp = {
376
- currentSchemaVersion: syncularGeneratedSchemaVersion,
377
- clientSchemaSupport: syncularGeneratedClientSchemaSupport,
378
- tables: syncularGeneratedAppTableRefs,
379
- clientSchemaForVersion: syncularGeneratedClientSchemaForVersion,
380
- tableSchemaForVersion: syncularGeneratedTableSchemaForVersion,
381
- projectClientRowForVersion: syncularProjectGeneratedClientRowForVersion,
382
- } as const;
383
-
384
- export type SyncularGeneratedAppTableName = keyof typeof syncularGeneratedApp.tables & string;
385
- export type SyncularGeneratedAppTableRef = typeof syncularGeneratedApp.tables[SyncularGeneratedAppTableName];
386
-
387
- function syncularResolveGeneratedAppTable(table: SyncularGeneratedAppTableName | SyncularGeneratedAppTableRef): SyncularGeneratedAppTableRef {
388
- return typeof table === 'string' ? syncularGeneratedApp.tables[table] : table;
389
- }
390
-
391
- function syncularGeneratedExtractScopes(table: string, row: Record<string, unknown>): StoredScopes {
392
- const tableSchema = syncularGeneratedTableSchemaForVersion(table, syncularGeneratedSchemaVersion);
393
- if (!tableSchema) throw new Error(`Unknown generated app table ${table}`);
394
- const scopes: StoredScopes = {};
395
- for (const scope of tableSchema.scopes) {
396
- const value = row[scope.column];
397
- if (value == null) {
398
- if (scope.required) throw new Error(`Missing required scope column ${table}.${scope.column}`);
399
- continue;
400
- }
401
- scopes[scope.name] = String(value);
402
- }
403
- return scopes;
404
- }
405
-
406
- function syncularGeneratedValidationErrorResult(opIndex: number, errors: readonly SyncularGeneratedValidationError[]): ApplyOperationResult {
407
- const response = createSyncularErrorResponse('sync.invalid_request', {
408
- message: errors.map((error) => `${error.path}: ${error.message}`).join('; '),
409
- });
410
- return {
411
- result: { opIndex, status: 'error', error: response.message ?? response.error, code: response.code, retriable: response.retryable },
412
- emittedChanges: [],
413
- };
414
- }
415
-
416
- function syncularAssertGeneratedEmittedChange(table: string, change: ApplyOperationResult['emittedChanges'][number]): void {
417
- if (change.table !== table) throw new Error(`Generated emitted change table ${change.table} does not match handler table ${table}`);
418
- if (change.op === 'delete') {
419
- if (change.row_json !== null) throw new Error(`Generated delete emitted change ${table}.${change.row_id} must not include row_json`);
420
- return;
421
- }
422
- const validation = syncularValidateGeneratedClientRow(table, change.row_json, syncularGeneratedSchemaVersion);
423
- if (!validation.ok) {
424
- throw new Error(`Generated emitted change row_json ${table}.${change.row_id} does not match current schema ${syncularGeneratedSchemaVersion}: ${validation.errors.map((error) => `${error.path}: ${error.message}`).join('; ')}`);
425
- }
426
- }
427
-
428
- function syncularAssertGeneratedApplyOperationResult(table: string, result: ApplyOperationResult, schemaVersion: number | null | undefined): void {
429
- for (const change of result.emittedChanges) syncularAssertGeneratedEmittedChange(table, change);
430
- if (result.result.status !== 'conflict') return;
431
- const validation = syncularValidateGeneratedClientRow(table, result.result.server_row, schemaVersion);
432
- if (!validation.ok) {
433
- throw new Error(`Generated conflict server_row ${table} does not match client schema ${schemaVersion}: ${validation.errors.map((error) => `${error.path}: ${error.message}`).join('; ')}`);
434
- }
435
- }
436
-
437
- export interface CreateSyncularAppServerHandlerOptions<DB extends SyncCoreDb = SyncCoreDb, Auth extends SyncServerAuth = SyncServerAuth> {
438
- table: SyncularGeneratedAppTableName | SyncularGeneratedAppTableRef;
439
- dependsOn?: string[];
440
- snapshotChunkTtlMs?: number;
441
- canRejectSingleOperationWithoutSavepoint?: boolean;
442
- resolveScopes(ctx: ServerContext<DB, Auth>): Promise<ScopeValues> | ScopeValues;
443
- extractScopes?: (row: Record<string, unknown>) => StoredScopes;
444
- snapshot(ctx: ServerSnapshotContext<DB, string, Auth>, params: Record<string, unknown> | undefined): Promise<{ rows: unknown[]; nextCursor: string | null }>;
445
- applyOperation(ctx: ServerApplyOperationContext<DB, Auth>, op: SyncOperation, opIndex: number): Promise<ApplyOperationResult>;
446
- applyOperationBatch?(ctx: ServerApplyOperationContext<DB, Auth>, operations: { op: SyncOperation; opIndex: number }[]): Promise<ApplyOperationResult[]>;
447
- }
448
-
449
- export function createSyncularAppServerHandler<DB extends SyncCoreDb = SyncCoreDb, Auth extends SyncServerAuth = SyncServerAuth>(options: CreateSyncularAppServerHandlerOptions<DB, Auth>): ServerTableHandler<DB, Auth> {
450
- const table = syncularResolveGeneratedAppTable(options.table);
451
- const tableName = table.name;
452
- const applyOperationBatch = options.applyOperationBatch;
453
- const handler: ServerTableHandler<DB, Auth> = {
454
- table: tableName,
455
- primaryKeyColumn: table.primaryKeyColumn,
456
- scopePatterns: [...table.scopePatterns],
457
- dependsOn: options.dependsOn,
458
- snapshotChunkTtlMs: options.snapshotChunkTtlMs,
459
- snapshotBinaryColumns: syncularGeneratedSnapshotBinaryColumns[tableName as keyof SyncularAppDb],
460
- snapshotBinaryEncoder: syncularGeneratedSnapshotBinaryEncoders[tableName as keyof SyncularAppDb],
461
- snapshotBinaryColumnsForVersion: (schemaVersion) => syncularGeneratedSnapshotBinaryColumnsForVersion(tableName, schemaVersion),
462
- snapshotBinaryEncoderForVersion: (schemaVersion) => syncularGeneratedSnapshotBinaryEncoderForVersion(tableName, schemaVersion),
463
- projectChangeForVersion(change, schemaVersion) {
464
- if (change.row_json == null) return change;
465
- if (!syncularGeneratedIsRecord(change.row_json)) throw new Error(`Generated pull change row_json ${tableName}.${change.row_id} must be an object`);
466
- const row = syncularProjectGeneratedClientRowForVersion(tableName, change.row_json, schemaVersion);
467
- const validation = syncularValidateGeneratedClientRow(tableName, row, schemaVersion);
468
- if (!validation.ok) throw new Error(`Generated pull change row_json ${tableName}.${change.row_id} does not match client schema ${schemaVersion}: ${validation.errors.map((error) => `${error.path}: ${error.message}`).join('; ')}`);
469
- return { ...change, row_json: row };
470
- },
471
- canRejectSingleOperationWithoutSavepoint: options.canRejectSingleOperationWithoutSavepoint,
472
- resolveScopes: async (ctx) => options.resolveScopes(ctx),
473
- extractScopes: options.extractScopes ?? ((row) => syncularGeneratedExtractScopes(tableName, row)),
474
- async snapshot(ctx, params) {
475
- if (!syncularIsSupportedClientSchemaVersion(ctx.schemaVersion)) {
476
- throw new SyncClientSchemaUnsupportedError({ schemaVersion: ctx.schemaVersion, supportedSchemaVersions: syncularGeneratedClientSchemaSupport.supported });
477
- }
478
- const page = await options.snapshot(ctx, params);
479
- const rows = page.rows ?? [];
480
- for (let index = 0; index < rows.length; index += 1) {
481
- const validation = syncularValidateGeneratedClientRow(tableName, rows[index], ctx.schemaVersion);
482
- if (!validation.ok) {
483
- throw new Error(`Generated snapshot row ${tableName}[${index}] does not match client schema ${ctx.schemaVersion}: ${validation.errors.map((error) => `${error.path}: ${error.message}`).join('; ')}`);
484
- }
485
- }
486
- return page;
487
- },
488
- async applyOperation(ctx, op, opIndex) {
489
- if (!syncularIsSupportedClientSchemaVersion(ctx.schemaVersion)) {
490
- return syncularUnsupportedClientSchemaResult({ opIndex, schemaVersion: ctx.schemaVersion });
491
- }
492
- const validation = syncularValidateGeneratedOperation(tableName, op, ctx.schemaVersion);
493
- if (!validation.ok) return syncularGeneratedValidationErrorResult(opIndex, validation.errors);
494
- const result = await options.applyOperation(ctx, op, opIndex);
495
- syncularAssertGeneratedApplyOperationResult(tableName, result, ctx.schemaVersion);
496
- return result;
497
- },
498
- applyOperationBatch: applyOperationBatch ? async (ctx, operations) => {
499
- for (const { op, opIndex } of operations) {
500
- if (!syncularIsSupportedClientSchemaVersion(ctx.schemaVersion)) {
501
- return [syncularUnsupportedClientSchemaResult({ opIndex, schemaVersion: ctx.schemaVersion })];
502
- }
503
- const validation = syncularValidateGeneratedOperation(tableName, op, ctx.schemaVersion);
504
- if (!validation.ok) return [syncularGeneratedValidationErrorResult(opIndex, validation.errors)];
505
- }
506
- const results = await applyOperationBatch(ctx, operations);
507
- for (const result of results) syncularAssertGeneratedApplyOperationResult(tableName, result, ctx.schemaVersion);
508
- return results;
509
- } : undefined,
510
- };
511
- return handler;
512
- }
@@ -1,5 +0,0 @@
1
- import { createRoot } from 'react-dom/client';
2
- import { App } from './app';
3
- import './styles.css';
4
-
5
- createRoot(document.getElementById('root')!).render(<App />);
@@ -1,129 +0,0 @@
1
- import { mkdirSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { createDatabase } from '@syncular/core';
4
- import {
5
- createServerHandler,
6
- ensureSyncSchema,
7
- type SyncCoreDb,
8
- } from '@syncular/server';
9
- import { createBunSqliteDialect } from '@syncular/server/bun-sqlite';
10
- import { createSyncServer } from '@syncular/server/hono';
11
- import { createSqliteServerDialect } from '@syncular/server/sqlite';
12
- import { Hono } from 'hono';
13
- import { upgradeWebSocket, websocket } from 'hono/bun';
14
- import type { Kysely } from 'kysely';
15
- // Regenerate the generated modules with `bun run codegen` after changing
16
- // migrations/ or syncular.app.ts.
17
- import { syncularGeneratedCodecs } from '../generated/syncular.generated';
18
- import type { SyncularAppDb } from '../generated/syncular.server.generated';
19
-
20
- interface AppServerDb extends SyncCoreDb, SyncularAppDb {}
21
-
22
- interface AppSyncServer {
23
- origin: string;
24
- close(): Promise<void>;
25
- }
26
-
27
- /**
28
- * Demo auth: every request authenticates as the same single user via a static
29
- * bearer token. Replace `authenticate` with your real session/token check and
30
- * derive `actorId` from it.
31
- */
32
- const DEMO_TOKEN = 'demo-user';
33
- const DEMO_ACTOR_ID = 'demo-user';
34
-
35
- export async function startSyncServer(
36
- options: { port?: number; databasePath?: string } = {}
37
- ): Promise<AppSyncServer> {
38
- const databasePath =
39
- options.databasePath ??
40
- process.env.SYNC_DB_PATH ??
41
- path.resolve(import.meta.dir, '../../data/sync.sqlite');
42
- if (databasePath !== ':memory:') {
43
- mkdirSync(path.dirname(databasePath), { recursive: true });
44
- }
45
-
46
- const dialect = createSqliteServerDialect();
47
- const db = createDatabase<AppServerDb>({
48
- dialect: createBunSqliteDialect({ path: databasePath }),
49
- family: 'sqlite',
50
- });
51
-
52
- await ensureSyncSchema(db, dialect);
53
- await ensureAppTables(db);
54
-
55
- const { syncRoutes } = createSyncServer<AppServerDb, { actorId: string }>({
56
- db,
57
- dialect,
58
- sync: {
59
- handlers: [
60
- createServerHandler<
61
- AppServerDb,
62
- SyncularAppDb,
63
- 'tasks',
64
- { actorId: string }
65
- >({
66
- table: 'tasks',
67
- scopes: ['user:{user_id}'],
68
- codecs: syncularGeneratedCodecs,
69
- resolveScopes: async (ctx) => ({ user_id: [ctx.actorId] }),
70
- }),
71
- ],
72
- authenticate: (request) => {
73
- const authorization = request.headers.get('authorization');
74
- const token = new URL(request.url).searchParams.get('token');
75
- if (authorization !== `Bearer ${DEMO_TOKEN}` && token !== DEMO_TOKEN) {
76
- return null;
77
- }
78
- return { actorId: DEMO_ACTOR_ID };
79
- },
80
- },
81
- routes: {
82
- cors: ['http://127.0.0.1:*', 'http://localhost:*'],
83
- rateLimit: false,
84
- websocket: {
85
- allowedOrigins: ['http://127.0.0.1:*', 'http://localhost:*'],
86
- heartbeatIntervalMs: 15_000,
87
- },
88
- },
89
- upgradeWebSocket,
90
- });
91
-
92
- const app = new Hono()
93
- .get('/health', (c) => c.json({ ok: true }))
94
- .route('/sync', syncRoutes);
95
-
96
- const server = Bun.serve({
97
- hostname: '127.0.0.1',
98
- port: options.port ?? 4100,
99
- fetch: app.fetch,
100
- websocket,
101
- idleTimeout: 0,
102
- });
103
-
104
- return {
105
- origin: `http://127.0.0.1:${server.port}`,
106
- async close() {
107
- server.stop(true);
108
- await db.destroy();
109
- },
110
- };
111
- }
112
-
113
- /**
114
- * The server owns its copy of the synced tables. Keep this in sync with
115
- * migrations/*.sql (the client installs the same schema locally from the
116
- * generated embedded migrations).
117
- */
118
- async function ensureAppTables(db: Kysely<AppServerDb>) {
119
- await db.schema
120
- .createTable('tasks')
121
- .ifNotExists()
122
- .addColumn('id', 'text', (col) => col.primaryKey())
123
- .addColumn('title', 'text', (col) => col.notNull())
124
- .addColumn('completed', 'integer', (col) => col.notNull().defaultTo(0))
125
- .addColumn('user_id', 'text', (col) => col.notNull())
126
- .addColumn('created_at', 'bigint', (col) => col.notNull().defaultTo(0))
127
- .addColumn('server_version', 'bigint', (col) => col.notNull().defaultTo(0))
128
- .execute();
129
- }