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,926 +0,0 @@
1
- // @generated by `cargo run --manifest-path rust/Cargo.toml -p syncular-codegen --`
2
- // Source: migrations/*.sql
3
-
4
- use serde_json::{json, Map, Value};
5
- pub use syncular_client::app_schema::{
6
- AppTableMetadata, ColumnMetadata, CrdtYjsFieldMetadata, EncryptedFieldMetadata, ScopeMetadata,
7
- ScopeSource,
8
- };
9
- #[allow(unused_imports)]
10
- use syncular_client::client::{
11
- SubscriptionSpec, SyncChangedRow, SyncularClientConfig, SyncularCommandHistoryExecutor,
12
- SyncularEncryptedCrdtMutationExecutor, SyncularLeasedMutationExecutor,
13
- SyncularMutationExecutor,
14
- };
15
- use syncular_client::command_history::{
16
- CommandHistoryEntry, CommandHistoryReceipt, CommandHistoryRecord, CommandHistoryState,
17
- };
18
- #[allow(unused_imports)]
19
- use syncular_client::crdt_yjs::{YjsUpdateEnvelope, YJS_PAYLOAD_KEY};
20
- use syncular_client::encryption::FieldEncryptionRule;
21
- use syncular_client::error::{Result, SyncularError};
22
- use syncular_client::protocol::{
23
- random_syncular_id, IntoSyncularMutation, MutationCommit, MutationReceipt,
24
- PendingSyncularMutation, SyncOperation, SyncularMutationBatch, SyncularMutationKind,
25
- };
26
-
27
- pub const APP_TABLES: &[&str] = &["tasks"];
28
-
29
- pub const TASKS_TABLE: &str = "tasks";
30
-
31
- pub const TASKS_COLUMNS: &[ColumnMetadata] = &[
32
- ColumnMetadata {
33
- name: "id",
34
- type_family: "text",
35
- notnull_required: false,
36
- primary_key: true,
37
- },
38
- ColumnMetadata {
39
- name: "title",
40
- type_family: "text",
41
- notnull_required: true,
42
- primary_key: false,
43
- },
44
- ColumnMetadata {
45
- name: "completed",
46
- type_family: "integer",
47
- notnull_required: true,
48
- primary_key: false,
49
- },
50
- ColumnMetadata {
51
- name: "user_id",
52
- type_family: "text",
53
- notnull_required: true,
54
- primary_key: false,
55
- },
56
- ColumnMetadata {
57
- name: "created_at",
58
- type_family: "integer",
59
- notnull_required: true,
60
- primary_key: false,
61
- },
62
- ColumnMetadata {
63
- name: "server_version",
64
- type_family: "integer",
65
- notnull_required: true,
66
- primary_key: false,
67
- },
68
- ];
69
-
70
- pub const TASKS_BLOB_COLUMNS: &[&str] = &[];
71
-
72
- pub const TASKS_CRDT_YJS_FIELDS: &[CrdtYjsFieldMetadata] = &[];
73
-
74
- pub const TASKS_ENCRYPTED_FIELDS: &[EncryptedFieldMetadata] = &[];
75
-
76
- pub const TASKS_SCOPES: &[ScopeMetadata] = &[ScopeMetadata {
77
- name: "user_id",
78
- column: "user_id",
79
- source: ScopeSource::ActorId,
80
- required: true,
81
- }];
82
-
83
- pub const TASKS_METADATA: AppTableMetadata = AppTableMetadata {
84
- name: "tasks",
85
- primary_key_column: "id",
86
- server_version_column: "server_version",
87
- soft_delete_column: None,
88
- subscription_id: "sub-tasks",
89
- columns: TASKS_COLUMNS,
90
- blob_columns: TASKS_BLOB_COLUMNS,
91
- crdt_yjs_fields: TASKS_CRDT_YJS_FIELDS,
92
- encrypted_fields: TASKS_ENCRYPTED_FIELDS,
93
- scopes: TASKS_SCOPES,
94
- };
95
-
96
- pub const APP_TABLE_METADATA: &[AppTableMetadata] = &[TASKS_METADATA];
97
-
98
- pub fn table_metadata(table: &str) -> Option<&'static AppTableMetadata> {
99
- APP_TABLE_METADATA
100
- .iter()
101
- .find(|metadata| metadata.name == table)
102
- }
103
-
104
- #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
105
- pub struct TaskChangedFields {
106
- pub id: bool,
107
- pub title: bool,
108
- pub completed: bool,
109
- pub user_id: bool,
110
- pub created_at: bool,
111
- pub server_version: bool,
112
- }
113
-
114
- impl TaskChangedFields {
115
- pub fn from_columns(columns: &[String]) -> Self {
116
- Self {
117
- id: columns.iter().any(|column| column == "id"),
118
- title: columns.iter().any(|column| column == "title"),
119
- completed: columns.iter().any(|column| column == "completed"),
120
- user_id: columns.iter().any(|column| column == "user_id"),
121
- created_at: columns.iter().any(|column| column == "created_at"),
122
- server_version: columns.iter().any(|column| column == "server_version"),
123
- }
124
- }
125
-
126
- pub fn contains(&self, column: &str) -> bool {
127
- match column {
128
- "id" => self.id,
129
- "title" => self.title,
130
- "completed" => self.completed,
131
- "user_id" => self.user_id,
132
- "created_at" => self.created_at,
133
- "server_version" => self.server_version,
134
- _ => false,
135
- }
136
- }
137
- }
138
-
139
- #[derive(Debug, Clone, Copy)]
140
- pub struct TaskChangedRow<'a> {
141
- pub raw: &'a SyncChangedRow,
142
- pub changed: TaskChangedFields,
143
- pub crdt: TaskChangedFields,
144
- }
145
-
146
- impl<'a> TaskChangedRow<'a> {
147
- pub fn from_raw(row: &'a SyncChangedRow) -> Option<Self> {
148
- if row.table != "tasks" {
149
- return None;
150
- }
151
- Some(Self {
152
- raw: row,
153
- changed: TaskChangedFields::from_columns(&row.changed_fields),
154
- crdt: TaskChangedFields::from_columns(&row.crdt_fields),
155
- })
156
- }
157
-
158
- pub fn row_id(&self) -> Option<&str> {
159
- self.raw.row_id.as_deref()
160
- }
161
-
162
- pub fn is_insert(&self) -> bool {
163
- self.raw.operation == "insert"
164
- }
165
-
166
- pub fn is_update(&self) -> bool {
167
- self.raw.operation == "update"
168
- }
169
-
170
- pub fn is_delete(&self) -> bool {
171
- self.raw.operation == "delete"
172
- }
173
- }
174
-
175
- pub fn task_changed_rows<'a>(
176
- rows: impl IntoIterator<Item = &'a SyncChangedRow>,
177
- ) -> Vec<TaskChangedRow<'a>> {
178
- rows.into_iter()
179
- .filter_map(TaskChangedRow::from_raw)
180
- .collect()
181
- }
182
-
183
- pub fn generated_field_encryption_rules() -> Vec<FieldEncryptionRule> {
184
- Vec::new()
185
- }
186
-
187
- pub fn default_subscriptions(config: &SyncularClientConfig) -> Vec<SubscriptionSpec> {
188
- vec![tasks_subscription(&config.actor_id)]
189
- }
190
-
191
- pub fn default_subscriptions_with_bootstrap_phases(
192
- config: &SyncularClientConfig,
193
- bootstrap_phases: &[(&str, i64)],
194
- ) -> Vec<SubscriptionSpec> {
195
- let mut subscriptions = default_subscriptions(config);
196
- apply_bootstrap_phases(&mut subscriptions, bootstrap_phases);
197
- subscriptions
198
- }
199
-
200
- pub fn apply_bootstrap_phases(
201
- subscriptions: &mut [SubscriptionSpec],
202
- bootstrap_phases: &[(&str, i64)],
203
- ) {
204
- for subscription in subscriptions {
205
- if let Some((_, phase)) = bootstrap_phases
206
- .iter()
207
- .find(|(key, _)| *key == subscription.id || *key == subscription.table)
208
- {
209
- subscription.bootstrap_phase = *phase;
210
- }
211
- }
212
- }
213
-
214
- pub fn with_bootstrap_phase(
215
- mut subscription: SubscriptionSpec,
216
- bootstrap_phase: i64,
217
- ) -> SubscriptionSpec {
218
- subscription.bootstrap_phase = bootstrap_phase;
219
- subscription
220
- }
221
-
222
- pub fn tasks_subscription(user_id: &str) -> SubscriptionSpec {
223
- let mut scopes = Map::new();
224
- scopes.insert("user_id".to_string(), json!(user_id));
225
- let params = Map::new();
226
-
227
- SubscriptionSpec {
228
- id: "sub-tasks".to_string(),
229
- table: "tasks".to_string(),
230
- scopes,
231
- params,
232
- bootstrap_phase: 0,
233
- }
234
- }
235
-
236
- #[derive(Debug, Clone)]
237
- pub struct NewTask {
238
- pub id: String,
239
- pub title: String,
240
- pub completed: i32,
241
- pub user_id: String,
242
- pub created_at: i64,
243
- }
244
-
245
- impl NewTask {
246
- pub fn new(id: &str, title: &str, user_id: &str) -> Self {
247
- Self {
248
- id: id.to_string(),
249
- title: title.to_string(),
250
- completed: 0,
251
- user_id: user_id.to_string(),
252
- created_at: 0,
253
- }
254
- }
255
-
256
- pub fn with_generated_id(title: &str, user_id: &str) -> Self {
257
- Self {
258
- id: random_syncular_id(),
259
- title: title.to_string(),
260
- completed: 0,
261
- user_id: user_id.to_string(),
262
- created_at: 0,
263
- }
264
- }
265
-
266
- pub fn row_json(&self) -> Value {
267
- let mut row = Map::new();
268
- row.insert("id".to_string(), json!(&self.id));
269
- row.insert("title".to_string(), json!(&self.title));
270
- row.insert("completed".to_string(), json!(&self.completed));
271
- row.insert("user_id".to_string(), json!(&self.user_id));
272
- row.insert("created_at".to_string(), json!(&self.created_at));
273
- Value::Object(row)
274
- }
275
-
276
- pub fn sync_operation(&self) -> SyncOperation {
277
- let mut payload = Map::new();
278
- payload.insert("title".to_string(), json!(&self.title));
279
- payload.insert("completed".to_string(), json!(&self.completed));
280
- payload.insert("user_id".to_string(), json!(&self.user_id));
281
- payload.insert("created_at".to_string(), json!(&self.created_at));
282
-
283
- SyncOperation {
284
- table: "tasks".to_string(),
285
- row_id: self.id.clone(),
286
- op: "upsert".to_string(),
287
- payload: Some(Value::Object(payload)),
288
- base_version: Some(0),
289
- }
290
- }
291
- }
292
-
293
- impl IntoSyncularMutation for NewTask {
294
- fn into_syncular_mutation(self) -> PendingSyncularMutation {
295
- let row_id = self.id.clone();
296
- PendingSyncularMutation {
297
- kind: SyncularMutationKind::Insert,
298
- table: "tasks".to_string(),
299
- row_id,
300
- payload: self.sync_operation().payload,
301
- base_version: None,
302
- local_row: Some(self.row_json()),
303
- }
304
- }
305
- }
306
-
307
- #[derive(Debug, Clone)]
308
- pub struct TaskPatch {
309
- row_id: String,
310
- base_version: Option<i64>,
311
- title: Option<String>,
312
- completed: Option<i32>,
313
- user_id: Option<String>,
314
- created_at: Option<i64>,
315
- }
316
-
317
- impl TaskPatch {
318
- pub fn new(row_id: &str) -> Self {
319
- Self {
320
- row_id: row_id.to_string(),
321
- base_version: None,
322
- title: None,
323
- completed: None,
324
- user_id: None,
325
- created_at: None,
326
- }
327
- }
328
-
329
- pub fn base_version(mut self, base_version: i64) -> Self {
330
- self.base_version = Some(base_version);
331
- self
332
- }
333
-
334
- pub fn title(mut self, title: &str) -> Self {
335
- self.title = Some(title.to_string());
336
- self
337
- }
338
-
339
- pub fn completed(mut self, completed: i32) -> Self {
340
- self.completed = Some(completed);
341
- self
342
- }
343
-
344
- pub fn user_id(mut self, user_id: &str) -> Self {
345
- self.user_id = Some(user_id.to_string());
346
- self
347
- }
348
-
349
- pub fn created_at(mut self, created_at: i64) -> Self {
350
- self.created_at = Some(created_at);
351
- self
352
- }
353
-
354
- pub fn payload_json(&self) -> Value {
355
- let mut payload = Map::new();
356
- if let Some(value) = &self.title {
357
- payload.insert("title".to_string(), json!(value));
358
- }
359
- if let Some(value) = &self.completed {
360
- payload.insert("completed".to_string(), json!(value));
361
- }
362
- if let Some(value) = &self.user_id {
363
- payload.insert("user_id".to_string(), json!(value));
364
- }
365
- if let Some(value) = &self.created_at {
366
- payload.insert("created_at".to_string(), json!(value));
367
- }
368
- Value::Object(payload)
369
- }
370
-
371
- pub fn sync_operation(&self) -> SyncOperation {
372
- SyncOperation {
373
- table: "tasks".to_string(),
374
- row_id: self.row_id.clone(),
375
- op: "upsert".to_string(),
376
- payload: Some(self.payload_json()),
377
- base_version: self.base_version,
378
- }
379
- }
380
- }
381
-
382
- impl IntoSyncularMutation for TaskPatch {
383
- fn into_syncular_mutation(self) -> PendingSyncularMutation {
384
- let payload = self.payload_json();
385
- PendingSyncularMutation {
386
- kind: SyncularMutationKind::Update,
387
- table: "tasks".to_string(),
388
- row_id: self.row_id,
389
- payload: Some(payload),
390
- base_version: self.base_version,
391
- local_row: None,
392
- }
393
- }
394
- }
395
-
396
- #[derive(Debug, Clone)]
397
- pub struct DeleteTask {
398
- row_id: String,
399
- base_version: Option<i64>,
400
- }
401
-
402
- impl DeleteTask {
403
- pub fn new(row_id: &str) -> Self {
404
- Self {
405
- row_id: row_id.to_string(),
406
- base_version: None,
407
- }
408
- }
409
-
410
- pub fn base_version(mut self, base_version: i64) -> Self {
411
- self.base_version = Some(base_version);
412
- self
413
- }
414
-
415
- pub fn sync_operation(&self) -> SyncOperation {
416
- SyncOperation {
417
- table: "tasks".to_string(),
418
- row_id: self.row_id.clone(),
419
- op: "delete".to_string(),
420
- payload: None,
421
- base_version: self.base_version,
422
- }
423
- }
424
- }
425
-
426
- impl IntoSyncularMutation for DeleteTask {
427
- fn into_syncular_mutation(self) -> PendingSyncularMutation {
428
- PendingSyncularMutation {
429
- kind: SyncularMutationKind::Delete,
430
- table: "tasks".to_string(),
431
- row_id: self.row_id,
432
- payload: None,
433
- base_version: self.base_version,
434
- local_row: None,
435
- }
436
- }
437
- }
438
-
439
- pub fn delete_task(row_id: &str, base_version: Option<i64>) -> SyncOperation {
440
- SyncOperation {
441
- table: "tasks".to_string(),
442
- row_id: row_id.to_string(),
443
- op: "delete".to_string(),
444
- payload: None,
445
- base_version,
446
- }
447
- }
448
-
449
- #[derive(Debug, Clone)]
450
- pub struct InsertReceipt {
451
- pub id: String,
452
- pub commit: MutationReceipt,
453
- }
454
-
455
- #[derive(Debug, Clone)]
456
- pub struct InsertManyReceipt {
457
- pub ids: Vec<String>,
458
- pub commit: MutationReceipt,
459
- }
460
-
461
- pub trait SyncularGeneratedMutationsExt: SyncularMutationExecutor {
462
- fn mutations(&mut self) -> SyncularAppMutations<'_, Self>
463
- where
464
- Self: Sized,
465
- {
466
- SyncularAppMutations { client: self }
467
- }
468
-
469
- fn leased_mutations(&mut self) -> SyncularAppLeasedMutations<'_, Self>
470
- where
471
- Self: Sized + SyncularLeasedMutationExecutor,
472
- {
473
- SyncularAppLeasedMutations { client: self }
474
- }
475
-
476
- fn command_history(&mut self) -> SyncularAppCommandHistory<'_, Self>
477
- where
478
- Self: Sized + SyncularCommandHistoryExecutor,
479
- {
480
- SyncularAppCommandHistory { client: self }
481
- }
482
-
483
- fn commit<R>(
484
- &mut self,
485
- f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
486
- ) -> Result<MutationCommit<R>>
487
- where
488
- Self: Sized,
489
- {
490
- let mut batch = SyncularMutationBatch::new();
491
- let result = {
492
- let mut tx = SyncularAppMutationTx { batch: &mut batch };
493
- f(&mut tx)?
494
- };
495
- let commit = self.apply_mutation_batch(batch)?;
496
- Ok(MutationCommit { result, commit })
497
- }
498
-
499
- fn commit_with_history<R>(
500
- &mut self,
501
- f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
502
- ) -> Result<MutationCommit<R>>
503
- where
504
- Self: Sized + SyncularCommandHistoryExecutor,
505
- {
506
- syncular_commit_with_history(self, "mutations", f)
507
- }
508
-
509
- fn commit_leased<R>(
510
- &mut self,
511
- f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
512
- ) -> Result<MutationCommit<R>>
513
- where
514
- Self: Sized + SyncularLeasedMutationExecutor,
515
- {
516
- let mut batch = SyncularMutationBatch::new();
517
- let result = {
518
- let mut tx = SyncularAppMutationTx { batch: &mut batch };
519
- f(&mut tx)?
520
- };
521
- let commit = self.apply_leased_mutation_batch(batch)?;
522
- Ok(MutationCommit { result, commit })
523
- }
524
-
525
- fn commit_leased_with_history<R>(
526
- &mut self,
527
- f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
528
- ) -> Result<MutationCommit<R>>
529
- where
530
- Self: Sized + SyncularCommandHistoryExecutor,
531
- {
532
- syncular_commit_with_history(self, "leasedMutations", f)
533
- }
534
- }
535
-
536
- impl<C> SyncularGeneratedMutationsExt for C where C: SyncularMutationExecutor {}
537
-
538
- pub struct SyncularAppMutations<'a, C: SyncularMutationExecutor + ?Sized> {
539
- client: &'a mut C,
540
- }
541
-
542
- pub struct SyncularAppLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
543
- client: &'a mut C,
544
- }
545
-
546
- pub struct SyncularAppMutationTx<'a> {
547
- batch: &'a mut SyncularMutationBatch,
548
- }
549
-
550
- pub struct SyncularAppCommandHistory<'a, C: SyncularCommandHistoryExecutor + ?Sized> {
551
- client: &'a mut C,
552
- }
553
-
554
- impl<C> SyncularAppCommandHistory<'_, C>
555
- where
556
- C: SyncularCommandHistoryExecutor + ?Sized,
557
- {
558
- pub fn can_undo(&mut self) -> Result<bool> {
559
- Ok(self
560
- .client
561
- .command_history_latest(CommandHistoryState::Done)?
562
- .is_some())
563
- }
564
-
565
- pub fn can_redo(&mut self) -> Result<bool> {
566
- Ok(self
567
- .client
568
- .command_history_latest(CommandHistoryState::Undone)?
569
- .is_some())
570
- }
571
-
572
- pub fn latest_undo(&mut self) -> Result<Option<CommandHistoryRecord>> {
573
- self.client
574
- .command_history_latest(CommandHistoryState::Done)
575
- }
576
-
577
- pub fn latest_redo(&mut self) -> Result<Option<CommandHistoryRecord>> {
578
- self.client
579
- .command_history_latest(CommandHistoryState::Undone)
580
- }
581
-
582
- pub fn undo_last(&mut self) -> Result<CommandHistoryReceipt> {
583
- syncular_replay_command_history(self.client, CommandHistoryState::Done)
584
- }
585
-
586
- pub fn redo_last(&mut self) -> Result<CommandHistoryReceipt> {
587
- syncular_replay_command_history(self.client, CommandHistoryState::Undone)
588
- }
589
- }
590
-
591
- fn syncular_commit_with_history<C, R>(
592
- client: &mut C,
593
- mutation_scope: &str,
594
- f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
595
- ) -> Result<MutationCommit<R>>
596
- where
597
- C: SyncularCommandHistoryExecutor + ?Sized,
598
- {
599
- let mut batch = SyncularMutationBatch::new();
600
- let result = {
601
- let mut tx = SyncularAppMutationTx { batch: &mut batch };
602
- f(&mut tx)?
603
- };
604
- let commit = client.apply_command_history_tracked_batch(mutation_scope, batch)?;
605
- Ok(MutationCommit { result, commit })
606
- }
607
-
608
- fn syncular_replay_command_history<C>(
609
- client: &mut C,
610
- state: CommandHistoryState,
611
- ) -> Result<CommandHistoryReceipt>
612
- where
613
- C: SyncularCommandHistoryExecutor + ?Sized,
614
- {
615
- let command = client.command_history_latest(state)?.ok_or_else(|| {
616
- syncular_command_history_error(
617
- "sync.command_history_empty",
618
- "there is no Syncular command to replay",
619
- )
620
- })?;
621
- syncular_assert_command_history_safe(&command)?;
622
- let undo = state == CommandHistoryState::Done;
623
- syncular_assert_command_history_current_rows(client, &command, undo)?;
624
-
625
- let mut batch = SyncularMutationBatch::new();
626
- if undo {
627
- for entry in command.entries.iter().rev() {
628
- batch.push(syncular_command_history_mutation_for_snapshot(
629
- &entry.table,
630
- &entry.row_id,
631
- &entry.before,
632
- )?);
633
- }
634
- } else {
635
- for entry in &command.entries {
636
- batch.push(syncular_command_history_mutation_for_snapshot(
637
- &entry.table,
638
- &entry.row_id,
639
- &entry.after,
640
- )?);
641
- }
642
- }
643
-
644
- let commit = client.apply_command_history_batch(&command.mutation_scope, batch)?;
645
- let next_state = if undo {
646
- CommandHistoryState::Undone
647
- } else {
648
- CommandHistoryState::Done
649
- };
650
- client.command_history_mark(&command.id, next_state, &commit)?;
651
- Ok(CommandHistoryReceipt {
652
- command_id: command.id,
653
- commit,
654
- })
655
- }
656
-
657
- fn syncular_assert_command_history_current_rows<C>(
658
- client: &mut C,
659
- command: &CommandHistoryRecord,
660
- undo: bool,
661
- ) -> Result<()>
662
- where
663
- C: SyncularCommandHistoryExecutor + ?Sized,
664
- {
665
- for entry in &command.entries {
666
- let current = client.command_history_current_row_json(&entry.table, &entry.row_id)?;
667
- let expected = if undo { &entry.after } else { &entry.before };
668
- if &current != expected {
669
- return Err(syncular_command_history_error(
670
- "sync.command_history_conflict",
671
- &format!(
672
- "cannot replay Syncular command {} because {}.{} changed since it was recorded",
673
- command.id, entry.table, entry.row_id
674
- ),
675
- ));
676
- }
677
- }
678
- Ok(())
679
- }
680
-
681
- fn syncular_assert_command_history_safe(command: &CommandHistoryRecord) -> Result<()> {
682
- for entry in &command.entries {
683
- let unsafe_fields = syncular_command_history_unsafe_fields(entry)?;
684
- if !unsafe_fields.is_empty() {
685
- return Err(syncular_command_history_error(
686
- "sync.command_history_unsafe_field",
687
- &format!(
688
- "cannot replay Syncular command {} because {}.{} changed unsafe fields: {}",
689
- command.id,
690
- entry.table,
691
- entry.row_id,
692
- unsafe_fields.join(", ")
693
- ),
694
- ));
695
- }
696
- }
697
- Ok(())
698
- }
699
-
700
- fn syncular_command_history_mutation_for_snapshot(
701
- table: &str,
702
- row_id: &str,
703
- snapshot: &Option<Value>,
704
- ) -> Result<PendingSyncularMutation> {
705
- match snapshot {
706
- None => Ok(PendingSyncularMutation {
707
- kind: SyncularMutationKind::Delete,
708
- table: table.to_string(),
709
- row_id: row_id.to_string(),
710
- payload: None,
711
- base_version: None,
712
- local_row: None,
713
- }),
714
- Some(snapshot) => Ok(PendingSyncularMutation {
715
- kind: SyncularMutationKind::Upsert,
716
- table: table.to_string(),
717
- row_id: row_id.to_string(),
718
- payload: Some(syncular_command_history_payload_for_snapshot(
719
- table, snapshot,
720
- )?),
721
- base_version: None,
722
- local_row: None,
723
- }),
724
- }
725
- }
726
-
727
- fn syncular_command_history_payload_for_snapshot(table: &str, snapshot: &Value) -> Result<Value> {
728
- let Some(object) = snapshot.as_object() else {
729
- return Err(syncular_command_history_error(
730
- "sync.command_history_conflict",
731
- "command history snapshot must be a JSON object",
732
- ));
733
- };
734
- let mut payload: Map<String, Value> = object.clone();
735
- match table {
736
- "tasks" => {
737
- payload.remove("id");
738
- payload.remove("server_version");
739
- Ok(())
740
- }
741
- _ => Err(syncular_command_history_error(
742
- "sync.command_history_table_unsupported",
743
- &format!("cannot replay undo history for unsupported table {table}"),
744
- )),
745
- }?;
746
- Ok(Value::Object(payload))
747
- }
748
-
749
- fn syncular_command_history_unsafe_fields(
750
- entry: &CommandHistoryEntry,
751
- ) -> Result<Vec<&'static str>> {
752
- #[allow(unused_mut)]
753
- let mut fields = Vec::new();
754
- match entry.table.as_str() {
755
- "tasks" => Ok(fields),
756
- _ => Err(syncular_command_history_error(
757
- "sync.command_history_table_unsupported",
758
- &format!(
759
- "cannot replay undo history for unsupported table {}",
760
- entry.table
761
- ),
762
- )),
763
- }
764
- }
765
-
766
- #[allow(dead_code)]
767
- fn syncular_command_history_push_unsafe_field(
768
- fields: &mut Vec<&'static str>,
769
- entry: &CommandHistoryEntry,
770
- field: &'static str,
771
- ) {
772
- if syncular_command_history_snapshot_field(&entry.before, field)
773
- != syncular_command_history_snapshot_field(&entry.after, field)
774
- && !fields.contains(&field)
775
- {
776
- fields.push(field);
777
- }
778
- }
779
-
780
- #[allow(dead_code)]
781
- fn syncular_command_history_snapshot_field<'a>(
782
- snapshot: &'a Option<Value>,
783
- field: &str,
784
- ) -> Option<&'a Value> {
785
- snapshot.as_ref()?.as_object()?.get(field)
786
- }
787
-
788
- fn syncular_command_history_error(code: &str, message: &str) -> SyncularError {
789
- SyncularError::config(format!("{code}: {message}"))
790
- }
791
-
792
- impl<'a, C> SyncularAppMutations<'a, C>
793
- where
794
- C: SyncularMutationExecutor + ?Sized,
795
- {
796
- pub fn tasks(self) -> TaskMutations<'a, C> {
797
- TaskMutations {
798
- client: self.client,
799
- }
800
- }
801
- }
802
-
803
- impl<'a, C> SyncularAppLeasedMutations<'a, C>
804
- where
805
- C: SyncularLeasedMutationExecutor + ?Sized,
806
- {
807
- pub fn tasks(self) -> TaskLeasedMutations<'a, C> {
808
- TaskLeasedMutations {
809
- client: self.client,
810
- }
811
- }
812
- }
813
-
814
- impl<'a> SyncularAppMutationTx<'a> {
815
- pub fn tasks(&mut self) -> TaskMutationTx<'_> {
816
- TaskMutationTx { batch: self.batch }
817
- }
818
- }
819
-
820
- pub struct TaskMutations<'a, C: SyncularMutationExecutor + ?Sized> {
821
- client: &'a mut C,
822
- }
823
-
824
- impl<C> TaskMutations<'_, C>
825
- where
826
- C: SyncularMutationExecutor + ?Sized,
827
- {
828
- pub fn insert(self, row: NewTask) -> Result<InsertReceipt> {
829
- let id = row.id.clone();
830
- let commit = self.client.apply_mutation(row)?;
831
- Ok(InsertReceipt { id, commit })
832
- }
833
-
834
- pub fn insert_many(self, rows: impl IntoIterator<Item = NewTask>) -> Result<InsertManyReceipt> {
835
- let mut batch = SyncularMutationBatch::new();
836
- let mut ids = Vec::new();
837
- for row in rows {
838
- ids.push(row.id.clone());
839
- batch.push(row);
840
- }
841
- let commit = self.client.apply_mutation_batch(batch)?;
842
- Ok(InsertManyReceipt { ids, commit })
843
- }
844
-
845
- pub fn update(self, patch: TaskPatch) -> Result<MutationReceipt> {
846
- self.client.apply_mutation(patch)
847
- }
848
-
849
- pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
850
- self.client.apply_mutation(DeleteTask::new(row_id))
851
- }
852
- }
853
-
854
- pub struct TaskLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
855
- client: &'a mut C,
856
- }
857
-
858
- impl<C> TaskLeasedMutations<'_, C>
859
- where
860
- C: SyncularLeasedMutationExecutor + ?Sized,
861
- {
862
- pub fn insert(self, row: NewTask) -> Result<InsertReceipt> {
863
- let id = row.id.clone();
864
- let commit = self.client.apply_leased_mutation(row)?;
865
- Ok(InsertReceipt { id, commit })
866
- }
867
-
868
- pub fn insert_many(self, rows: impl IntoIterator<Item = NewTask>) -> Result<InsertManyReceipt> {
869
- let mut batch = SyncularMutationBatch::new();
870
- let mut ids = Vec::new();
871
- for row in rows {
872
- ids.push(row.id.clone());
873
- batch.push(row);
874
- }
875
- let commit = self.client.apply_leased_mutation_batch(batch)?;
876
- Ok(InsertManyReceipt { ids, commit })
877
- }
878
-
879
- pub fn update(self, patch: TaskPatch) -> Result<MutationReceipt> {
880
- self.client.apply_leased_mutation(patch)
881
- }
882
-
883
- pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
884
- self.client.apply_leased_mutation(DeleteTask::new(row_id))
885
- }
886
- }
887
-
888
- pub struct TaskMutationTx<'a> {
889
- batch: &'a mut SyncularMutationBatch,
890
- }
891
-
892
- impl TaskMutationTx<'_> {
893
- pub fn insert(self, row: NewTask) -> Result<String> {
894
- let id = row.id.clone();
895
- self.batch.push(row);
896
- Ok(id)
897
- }
898
-
899
- pub fn insert_many(self, rows: impl IntoIterator<Item = NewTask>) -> Result<Vec<String>> {
900
- let mut ids = Vec::new();
901
- for row in rows {
902
- ids.push(row.id.clone());
903
- self.batch.push(row);
904
- }
905
- Ok(ids)
906
- }
907
-
908
- pub fn update(self, patch: TaskPatch) -> Result<()> {
909
- self.batch.push(patch);
910
- Ok(())
911
- }
912
-
913
- pub fn delete(self, row_id: &str) -> Result<()> {
914
- self.batch.push(DeleteTask::new(row_id));
915
- Ok(())
916
- }
917
- }
918
-
919
- pub mod prelude {
920
- pub use super::{
921
- DeleteTask, InsertManyReceipt, InsertReceipt, NewTask, SyncularAppCommandHistory,
922
- SyncularAppLeasedMutations, SyncularAppMutationTx, SyncularAppMutations,
923
- SyncularGeneratedMutationsExt, TaskLeasedMutations, TaskMutationTx, TaskMutations,
924
- TaskPatch,
925
- };
926
- }