@livestore/common 0.4.0-dev.1 → 0.4.0-dev.10

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 (253) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +7 -2
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
  4. package/dist/ClientSessionLeaderThreadProxy.js.map +1 -1
  5. package/dist/adapter-types.d.ts +9 -3
  6. package/dist/adapter-types.d.ts.map +1 -1
  7. package/dist/adapter-types.js.map +1 -1
  8. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  9. package/dist/devtools/devtools-messages-common.d.ts +7 -14
  10. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  11. package/dist/devtools/devtools-messages-common.js +1 -6
  12. package/dist/devtools/devtools-messages-common.js.map +1 -1
  13. package/dist/devtools/devtools-messages-leader.d.ts +27 -25
  14. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  15. package/dist/errors.d.ts +47 -5
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +22 -3
  18. package/dist/errors.js.map +1 -1
  19. package/dist/leader-thread/LeaderSyncProcessor.d.ts +7 -3
  20. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  21. package/dist/leader-thread/LeaderSyncProcessor.js +122 -49
  22. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  23. package/dist/leader-thread/eventlog.d.ts +4 -10
  24. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  25. package/dist/leader-thread/eventlog.js +4 -6
  26. package/dist/leader-thread/eventlog.js.map +1 -1
  27. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  28. package/dist/leader-thread/leader-worker-devtools.js +6 -2
  29. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  30. package/dist/leader-thread/make-leader-thread-layer.d.ts +1 -2
  31. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  32. package/dist/leader-thread/make-leader-thread-layer.js +68 -19
  33. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  34. package/dist/leader-thread/make-leader-thread-layer.test.d.ts +2 -0
  35. package/dist/leader-thread/make-leader-thread-layer.test.d.ts.map +1 -0
  36. package/dist/leader-thread/make-leader-thread-layer.test.js +32 -0
  37. package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -0
  38. package/dist/leader-thread/materialize-event.d.ts +2 -2
  39. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  40. package/dist/leader-thread/materialize-event.js +23 -9
  41. package/dist/leader-thread/materialize-event.js.map +1 -1
  42. package/dist/leader-thread/recreate-db.d.ts +2 -3
  43. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  44. package/dist/leader-thread/recreate-db.js +1 -1
  45. package/dist/leader-thread/recreate-db.js.map +1 -1
  46. package/dist/leader-thread/shutdown-channel.d.ts +2 -2
  47. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  48. package/dist/leader-thread/shutdown-channel.js +2 -2
  49. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  50. package/dist/leader-thread/types.d.ts +7 -5
  51. package/dist/leader-thread/types.d.ts.map +1 -1
  52. package/dist/leader-thread/types.js.map +1 -1
  53. package/dist/materializer-helper.d.ts +1 -1
  54. package/dist/materializer-helper.d.ts.map +1 -1
  55. package/dist/materializer-helper.js +20 -4
  56. package/dist/materializer-helper.js.map +1 -1
  57. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  58. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  59. package/dist/rematerialize-from-eventlog.js +25 -16
  60. package/dist/rematerialize-from-eventlog.js.map +1 -1
  61. package/dist/schema/EventDef.d.ts +3 -0
  62. package/dist/schema/EventDef.d.ts.map +1 -1
  63. package/dist/schema/EventDef.js.map +1 -1
  64. package/dist/schema/LiveStoreEvent.d.ts +1 -1
  65. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  66. package/dist/schema/LiveStoreEvent.js +1 -2
  67. package/dist/schema/LiveStoreEvent.js.map +1 -1
  68. package/dist/schema/mod.d.ts +2 -0
  69. package/dist/schema/mod.d.ts.map +1 -1
  70. package/dist/schema/mod.js +1 -0
  71. package/dist/schema/mod.js.map +1 -1
  72. package/dist/schema/schema.d.ts +15 -0
  73. package/dist/schema/schema.d.ts.map +1 -1
  74. package/dist/schema/schema.js +26 -1
  75. package/dist/schema/schema.js.map +1 -1
  76. package/dist/schema/state/sqlite/client-document-def.d.ts +35 -5
  77. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  78. package/dist/schema/state/sqlite/client-document-def.js +95 -4
  79. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  80. package/dist/schema/state/sqlite/client-document-def.test.js +16 -0
  81. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  82. package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
  83. package/dist/schema/state/sqlite/column-annotations.js +14 -6
  84. package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
  85. package/dist/schema/state/sqlite/column-def.d.ts +6 -2
  86. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
  87. package/dist/schema/state/sqlite/column-def.js +122 -185
  88. package/dist/schema/state/sqlite/column-def.js.map +1 -1
  89. package/dist/schema/state/sqlite/column-def.test.js +116 -73
  90. package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
  91. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
  92. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  93. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +23 -6
  94. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  95. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  96. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +2 -1
  97. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  98. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  99. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  100. package/dist/schema/state/sqlite/mod.js +1 -1
  101. package/dist/schema/state/sqlite/mod.js.map +1 -1
  102. package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
  103. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  104. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  105. package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
  106. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  107. package/dist/schema/state/sqlite/query-builder/impl.test.js +137 -2
  108. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  109. package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
  110. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  111. package/dist/schema/state/sqlite/system-tables.js +2 -0
  112. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  113. package/dist/schema/state/sqlite/table-def.d.ts +4 -4
  114. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  115. package/dist/schema/state/sqlite/table-def.js +2 -2
  116. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  117. package/dist/schema/state/sqlite/table-def.test.js +51 -2
  118. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  119. package/dist/schema/unknown-events.d.ts +47 -0
  120. package/dist/schema/unknown-events.d.ts.map +1 -0
  121. package/dist/schema/unknown-events.js +69 -0
  122. package/dist/schema/unknown-events.js.map +1 -0
  123. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  124. package/dist/sql-queries/sql-query-builder.js +2 -1
  125. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  126. package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -11
  127. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  128. package/dist/sync/ClientSessionSyncProcessor.js +35 -33
  129. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  130. package/dist/sync/errors.d.ts +61 -0
  131. package/dist/sync/errors.d.ts.map +1 -0
  132. package/dist/sync/errors.js +36 -0
  133. package/dist/sync/errors.js.map +1 -0
  134. package/dist/sync/index.d.ts +3 -0
  135. package/dist/sync/index.d.ts.map +1 -1
  136. package/dist/sync/index.js +3 -0
  137. package/dist/sync/index.js.map +1 -1
  138. package/dist/sync/mock-sync-backend.d.ts +23 -0
  139. package/dist/sync/mock-sync-backend.d.ts.map +1 -0
  140. package/dist/sync/mock-sync-backend.js +114 -0
  141. package/dist/sync/mock-sync-backend.js.map +1 -0
  142. package/dist/sync/next/compact-events.d.ts.map +1 -1
  143. package/dist/sync/next/compact-events.js +4 -5
  144. package/dist/sync/next/compact-events.js.map +1 -1
  145. package/dist/sync/next/facts.d.ts.map +1 -1
  146. package/dist/sync/next/facts.js +1 -2
  147. package/dist/sync/next/facts.js.map +1 -1
  148. package/dist/sync/next/history-dag-common.d.ts +50 -11
  149. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  150. package/dist/sync/next/history-dag-common.js +193 -4
  151. package/dist/sync/next/history-dag-common.js.map +1 -1
  152. package/dist/sync/next/history-dag.d.ts.map +1 -1
  153. package/dist/sync/next/history-dag.js +3 -1
  154. package/dist/sync/next/history-dag.js.map +1 -1
  155. package/dist/sync/sync-backend-kv.d.ts +7 -0
  156. package/dist/sync/sync-backend-kv.d.ts.map +1 -0
  157. package/dist/sync/sync-backend-kv.js +18 -0
  158. package/dist/sync/sync-backend-kv.js.map +1 -0
  159. package/dist/sync/sync-backend.d.ts +105 -0
  160. package/dist/sync/sync-backend.d.ts.map +1 -0
  161. package/dist/sync/sync-backend.js +61 -0
  162. package/dist/sync/sync-backend.js.map +1 -0
  163. package/dist/sync/sync.d.ts +6 -84
  164. package/dist/sync/sync.d.ts.map +1 -1
  165. package/dist/sync/sync.js +2 -27
  166. package/dist/sync/sync.js.map +1 -1
  167. package/dist/sync/transport-chunking.d.ts +36 -0
  168. package/dist/sync/transport-chunking.d.ts.map +1 -0
  169. package/dist/sync/transport-chunking.js +56 -0
  170. package/dist/sync/transport-chunking.js.map +1 -0
  171. package/dist/sync/validate-push-payload.d.ts +1 -1
  172. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  173. package/dist/sync/validate-push-payload.js +6 -6
  174. package/dist/sync/validate-push-payload.js.map +1 -1
  175. package/dist/testing/event-factory.d.ts +68 -0
  176. package/dist/testing/event-factory.d.ts.map +1 -0
  177. package/dist/testing/event-factory.js +80 -0
  178. package/dist/testing/event-factory.js.map +1 -0
  179. package/dist/testing/mod.d.ts +2 -0
  180. package/dist/testing/mod.d.ts.map +1 -0
  181. package/dist/testing/mod.js +2 -0
  182. package/dist/testing/mod.js.map +1 -0
  183. package/dist/version.d.ts +2 -2
  184. package/dist/version.d.ts.map +1 -1
  185. package/dist/version.js +2 -2
  186. package/dist/version.js.map +1 -1
  187. package/package.json +7 -8
  188. package/src/ClientSessionLeaderThreadProxy.ts +7 -2
  189. package/src/adapter-types.ts +13 -3
  190. package/src/devtools/devtools-messages-common.ts +1 -8
  191. package/src/errors.ts +33 -4
  192. package/src/leader-thread/LeaderSyncProcessor.ts +179 -57
  193. package/src/leader-thread/eventlog.ts +10 -6
  194. package/src/leader-thread/leader-worker-devtools.ts +6 -2
  195. package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
  196. package/src/leader-thread/make-leader-thread-layer.ts +137 -26
  197. package/src/leader-thread/materialize-event.ts +34 -9
  198. package/src/leader-thread/recreate-db.ts +11 -3
  199. package/src/leader-thread/shutdown-channel.ts +16 -2
  200. package/src/leader-thread/types.ts +7 -5
  201. package/src/materializer-helper.ts +22 -5
  202. package/src/rematerialize-from-eventlog.ts +33 -23
  203. package/src/schema/EventDef.ts +3 -0
  204. package/src/schema/LiveStoreEvent.ts +1 -2
  205. package/src/schema/mod.ts +2 -0
  206. package/src/schema/schema.ts +37 -1
  207. package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
  208. package/src/schema/state/sqlite/client-document-def.ts +117 -5
  209. package/src/schema/state/sqlite/column-annotations.ts +16 -6
  210. package/src/schema/state/sqlite/column-def.test.ts +150 -93
  211. package/src/schema/state/sqlite/column-def.ts +128 -203
  212. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
  213. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
  214. package/src/schema/state/sqlite/mod.ts +1 -0
  215. package/src/schema/state/sqlite/query-builder/api.ts +7 -2
  216. package/src/schema/state/sqlite/query-builder/impl.test.ts +187 -6
  217. package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
  218. package/src/schema/state/sqlite/system-tables.ts +2 -0
  219. package/src/schema/state/sqlite/table-def.test.ts +64 -2
  220. package/src/schema/state/sqlite/table-def.ts +9 -8
  221. package/src/schema/unknown-events.ts +131 -0
  222. package/src/sql-queries/sql-query-builder.ts +2 -1
  223. package/src/sync/ClientSessionSyncProcessor.ts +55 -49
  224. package/src/sync/errors.ts +38 -0
  225. package/src/sync/index.ts +3 -0
  226. package/src/sync/mock-sync-backend.ts +184 -0
  227. package/src/sync/next/compact-events.ts +4 -5
  228. package/src/sync/next/facts.ts +1 -3
  229. package/src/sync/next/history-dag-common.ts +272 -21
  230. package/src/sync/next/history-dag.ts +3 -1
  231. package/src/sync/sync-backend-kv.ts +22 -0
  232. package/src/sync/sync-backend.ts +185 -0
  233. package/src/sync/sync.ts +6 -89
  234. package/src/sync/transport-chunking.ts +90 -0
  235. package/src/sync/validate-push-payload.ts +6 -7
  236. package/src/testing/event-factory.ts +133 -0
  237. package/src/testing/mod.ts +1 -0
  238. package/src/version.ts +2 -2
  239. package/dist/schema-management/migrations.test.d.ts +0 -2
  240. package/dist/schema-management/migrations.test.d.ts.map +0 -1
  241. package/dist/schema-management/migrations.test.js +0 -52
  242. package/dist/schema-management/migrations.test.js.map +0 -1
  243. package/dist/sync/next/graphology.d.ts +0 -8
  244. package/dist/sync/next/graphology.d.ts.map +0 -1
  245. package/dist/sync/next/graphology.js +0 -30
  246. package/dist/sync/next/graphology.js.map +0 -1
  247. package/dist/sync/next/graphology_.d.ts +0 -3
  248. package/dist/sync/next/graphology_.d.ts.map +0 -1
  249. package/dist/sync/next/graphology_.js +0 -3
  250. package/dist/sync/next/graphology_.js.map +0 -1
  251. package/src/sync/next/ambient.d.ts +0 -3
  252. package/src/sync/next/graphology.ts +0 -41
  253. package/src/sync/next/graphology_.ts +0 -2
@@ -409,9 +409,7 @@ describe('query builder', () => {
409
409
  })
410
410
 
411
411
  it('should handle INSERT queries with undefined values', () => {
412
- expect(
413
- dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active', completed: undefined })),
414
- ).toMatchInlineSnapshot(`
412
+ expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }))).toMatchInlineSnapshot(`
415
413
  {
416
414
  "bindValues": [
417
415
  "123",
@@ -478,9 +476,7 @@ describe('query builder', () => {
478
476
  })
479
477
 
480
478
  it('should handle UPDATE queries with undefined values', () => {
481
- expect(
482
- dump(db.todos.update({ status: undefined, text: 'some text' }).where({ id: '123' })),
483
- ).toMatchInlineSnapshot(`
479
+ expect(dump(db.todos.update({ text: 'some text' }).where({ id: '123' }))).toMatchInlineSnapshot(`
484
480
  {
485
481
  "bindValues": [
486
482
  "some text",
@@ -622,6 +618,191 @@ describe('query builder', () => {
622
618
  }
623
619
  `)
624
620
  })
621
+
622
+ it('should handle where().delete() - preserving where clauses', () => {
623
+ expect(dump(db.todos.where({ status: 'completed' }).delete())).toMatchInlineSnapshot(`
624
+ {
625
+ "bindValues": [
626
+ "completed",
627
+ ],
628
+ "query": "DELETE FROM 'todos' WHERE status = ?",
629
+ "schema": "number",
630
+ }
631
+ `)
632
+
633
+ // Multiple where clauses
634
+ expect(dump(db.todos.where({ status: 'completed' }).where({ deletedAt: null }).delete())).toMatchInlineSnapshot(`
635
+ {
636
+ "bindValues": [
637
+ "completed",
638
+ ],
639
+ "query": "DELETE FROM 'todos' WHERE status = ? AND deletedAt IS NULL",
640
+ "schema": "number",
641
+ }
642
+ `)
643
+ })
644
+
645
+ it('should handle where().update() - preserving where clauses', () => {
646
+ expect(dump(db.todos.where({ id: '123' }).update({ status: 'completed' }))).toMatchInlineSnapshot(`
647
+ {
648
+ "bindValues": [
649
+ "completed",
650
+ "123",
651
+ ],
652
+ "query": "UPDATE 'todos' SET status = ? WHERE id = ?",
653
+ "schema": "number",
654
+ }
655
+ `)
656
+
657
+ // Multiple where clauses
658
+ expect(
659
+ dump(db.todos.where({ id: '123' }).where({ deletedAt: null }).update({ status: 'completed' })),
660
+ ).toMatchInlineSnapshot(`
661
+ {
662
+ "bindValues": [
663
+ "completed",
664
+ "123",
665
+ ],
666
+ "query": "UPDATE 'todos' SET status = ? WHERE id = ? AND deletedAt IS NULL",
667
+ "schema": "number",
668
+ }
669
+ `)
670
+ })
671
+
672
+ it('should have equivalent behavior for both delete patterns', () => {
673
+ const pattern1 = dump(db.todos.where({ status: 'completed', id: '123' }).delete())
674
+ const pattern2 = dump(db.todos.delete().where({ status: 'completed', id: '123' }))
675
+
676
+ expect(pattern1).toEqual(pattern2)
677
+ })
678
+
679
+ it('should have equivalent behavior for both update patterns', () => {
680
+ const pattern1 = dump(db.todos.where({ id: '123' }).update({ status: 'completed', text: 'Updated' }))
681
+ const pattern2 = dump(db.todos.update({ status: 'completed', text: 'Updated' }).where({ id: '123' }))
682
+
683
+ expect(pattern1).toEqual(pattern2)
684
+ })
685
+ })
686
+
687
+ describe('schema transforms', () => {
688
+ const Flat = Schema.Struct({
689
+ id: Schema.String.pipe(State.SQLite.withPrimaryKey),
690
+ contactFirstName: Schema.String,
691
+ contactLastName: Schema.String,
692
+ contactEmail: Schema.String.pipe(State.SQLite.withUnique),
693
+ })
694
+
695
+ const Nested = Schema.transform(
696
+ Flat,
697
+ Schema.Struct({
698
+ id: Schema.String,
699
+ contact: Schema.Struct({
700
+ firstName: Schema.String,
701
+ lastName: Schema.String,
702
+ email: Schema.String,
703
+ }),
704
+ }),
705
+ {
706
+ decode: ({ id, contactFirstName, contactLastName, contactEmail }) => ({
707
+ id,
708
+ contact: {
709
+ firstName: contactFirstName,
710
+ lastName: contactLastName,
711
+ email: contactEmail,
712
+ },
713
+ }),
714
+ encode: ({ id, contact }) => ({
715
+ id,
716
+ contactFirstName: contact.firstName,
717
+ contactLastName: contact.lastName,
718
+ contactEmail: contact.email,
719
+ }),
720
+ },
721
+ )
722
+
723
+ const makeContactsTable = () =>
724
+ State.SQLite.table({
725
+ name: 'contacts',
726
+ schema: Nested,
727
+ // schema: Flat,
728
+ })
729
+
730
+ it('exposes flattened insert type while schema type is nested', () => {
731
+ const contactsTable = makeContactsTable()
732
+
733
+ type InsertInput = Parameters<(typeof contactsTable)['insert']>[0]
734
+ type NestedType = Schema.Schema.Type<typeof Nested>
735
+
736
+ type Assert<T extends true> = T
737
+
738
+ type InsertKeys = keyof InsertInput
739
+ type NestedKeys = keyof NestedType
740
+
741
+ type _InsertHasFlattenedColumns = Assert<
742
+ 'contactFirstName' extends InsertKeys
743
+ ? 'contactLastName' extends InsertKeys
744
+ ? 'contactEmail' extends InsertKeys
745
+ ? true
746
+ : false
747
+ : false
748
+ : false
749
+ >
750
+
751
+ type _InsertDoesNotExposeNested = Assert<Extract<'contact', InsertKeys> extends never ? true : false>
752
+
753
+ type _SchemaTypeIsNested = Assert<'contact' extends NestedKeys ? true : false>
754
+
755
+ void contactsTable
756
+ })
757
+
758
+ it('fails to encode nested inserts because flat columns are required', () => {
759
+ const contactsTable = makeContactsTable()
760
+
761
+ expect(
762
+ contactsTable
763
+ // TODO in the future we should use decoded types here instead of encoded
764
+ .insert({
765
+ id: 'person-1',
766
+ contactFirstName: 'Ada',
767
+ contactLastName: 'Lovelace',
768
+ contactEmail: 'ada@example.com',
769
+ })
770
+ .asSql(),
771
+ ).toMatchInlineSnapshot(`
772
+ {
773
+ "bindValues": [
774
+ "person-1",
775
+ "Ada",
776
+ "Lovelace",
777
+ "ada@example.com",
778
+ ],
779
+ "query": "INSERT INTO 'contacts' (id, contactFirstName, contactLastName, contactEmail) VALUES (?, ?, ?, ?)",
780
+ "usedTables": Set {
781
+ "contacts",
782
+ },
783
+ }
784
+ `)
785
+ })
786
+
787
+ it('fails to encode nested inserts because flat columns are required', () => {
788
+ const contactsTable = makeContactsTable()
789
+
790
+ expect(() =>
791
+ contactsTable
792
+ .insert({
793
+ id: 'person-1',
794
+ // @ts-expect-error
795
+ contact: {
796
+ firstName: 'Ada',
797
+ lastName: 'Lovelace',
798
+ email: 'ada@example.com',
799
+ },
800
+ })
801
+ .asSql(),
802
+ ).toThrowErrorMatchingInlineSnapshot(`
803
+ [ParseError: contacts\n└─ ["contactFirstName"]\n └─ is missing]
804
+ `)
805
+ })
625
806
  })
626
807
  })
627
808
 
@@ -219,21 +219,27 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
219
219
  update: (values) => {
220
220
  const filteredValues = Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined))
221
221
 
222
+ // Preserve where clauses if coming from a SelectQuery
223
+ const whereClause = ast._tag === 'SelectQuery' ? ast.where : []
224
+
222
225
  return makeQueryBuilder(tableDef, {
223
226
  _tag: 'UpdateQuery',
224
227
  tableDef,
225
228
  values: filteredValues,
226
- where: [],
229
+ where: whereClause,
227
230
  returning: undefined,
228
231
  resultSchema: Schema.Void,
229
232
  }) as any
230
233
  },
231
234
 
232
235
  delete: () => {
236
+ // Preserve where clauses if coming from a SelectQuery
237
+ const whereClause = ast._tag === 'SelectQuery' ? ast.where : []
238
+
233
239
  return makeQueryBuilder(tableDef, {
234
240
  _tag: 'DeleteQuery',
235
241
  tableDef,
236
- where: [],
242
+ where: whereClause,
237
243
  returning: undefined,
238
244
  resultSchema: Schema.Void,
239
245
  }) as any
@@ -96,6 +96,8 @@ export const syncStatusTable = table({
96
96
  name: SYNC_STATUS_TABLE,
97
97
  columns: {
98
98
  head: SqliteDsl.integer({ primaryKey: true }),
99
+ // Null means the sync backend is not yet connected and we haven't yet seen a backend ID
100
+ backendId: SqliteDsl.text({ nullable: true }),
99
101
  },
100
102
  })
101
103
 
@@ -63,6 +63,7 @@ describe('table function overloads', () => {
63
63
  id: State.SQLite.text({ primaryKey: true }),
64
64
  text: State.SQLite.text({ default: '' }),
65
65
  completed: State.SQLite.boolean({ default: false }),
66
+ optionalBoolean: State.SQLite.boolean({ default: false, nullable: true }),
66
67
  optionalComplex: State.SQLite.json({
67
68
  nullable: true,
68
69
  schema: Schema.Struct({ color: Schema.String }).pipe(Schema.UndefinedOr),
@@ -76,7 +77,15 @@ describe('table function overloads', () => {
76
77
  expect(todosTable.sqliteDef.columns).toHaveProperty('text')
77
78
  expect(todosTable.sqliteDef.columns).toHaveProperty('completed')
78
79
  expect(todosTable.sqliteDef.columns).toHaveProperty('optionalComplex')
80
+
81
+ expect(todosTable.sqliteDef.columns.optionalBoolean.nullable).toBe(true)
82
+ expect(todosTable.sqliteDef.columns.optionalBoolean.schema.toString()).toBe('(number <-> boolean) | null')
83
+ expect((todosTable.rowSchema as any).fields.optionalBoolean.toString()).toBe('(number <-> boolean) | null')
84
+
79
85
  expect(todosTable.sqliteDef.columns.optionalComplex.nullable).toBe(true)
86
+ expect(todosTable.sqliteDef.columns.optionalComplex.schema.toString()).toBe(
87
+ '(parseJson <-> { readonly color: string } | undefined) | null',
88
+ )
80
89
  expect((todosTable.rowSchema as any).fields.optionalComplex.toString()).toBe(
81
90
  '(parseJson <-> { readonly color: string } | undefined) | null',
82
91
  )
@@ -169,6 +178,59 @@ describe('table function overloads', () => {
169
178
  expect(userTable.sqliteDef.columns.age.columnType).toBe('integer')
170
179
  })
171
180
 
181
+ it('should support schemas that transform flat columns into nested types', () => {
182
+ const Flat = Schema.Struct({
183
+ id: Schema.String.pipe(State.SQLite.withPrimaryKey),
184
+ contactFirstName: Schema.String,
185
+ contactLastName: Schema.String,
186
+ contactEmail: Schema.String.pipe(State.SQLite.withUnique),
187
+ })
188
+
189
+ const Nested = Schema.transform(
190
+ Flat,
191
+ Schema.Struct({
192
+ id: Schema.String,
193
+ contact: Schema.Struct({
194
+ firstName: Schema.String,
195
+ lastName: Schema.String,
196
+ email: Schema.String,
197
+ }),
198
+ }),
199
+ {
200
+ decode: ({ id, contactFirstName, contactLastName, contactEmail }) => ({
201
+ id,
202
+ contact: {
203
+ firstName: contactFirstName,
204
+ lastName: contactLastName,
205
+ email: contactEmail,
206
+ },
207
+ }),
208
+ encode: ({ id, contact }) => ({
209
+ id,
210
+ contactFirstName: contact.firstName,
211
+ contactLastName: contact.lastName,
212
+ contactEmail: contact.email,
213
+ }),
214
+ },
215
+ )
216
+
217
+ const contactsTable = State.SQLite.table({
218
+ name: 'contacts',
219
+ schema: Nested,
220
+ })
221
+
222
+ const columns = contactsTable.sqliteDef.columns
223
+
224
+ expect(Object.keys(columns)).toEqual(['id', 'contactFirstName', 'contactLastName', 'contactEmail'])
225
+ expect(columns.id.primaryKey).toBe(true)
226
+ expect(columns.contactEmail.columnType).toBe('text')
227
+ expect(contactsTable.sqliteDef.indexes).toContainEqual({
228
+ name: 'idx_contacts_contactEmail_unique',
229
+ columns: ['contactEmail'],
230
+ isUnique: true,
231
+ })
232
+ })
233
+
172
234
  it('should extract table name from Schema.Class identifier', () => {
173
235
  class TodoItem extends Schema.Class<TodoItem>('TodoItem')({
174
236
  id: Schema.String,
@@ -217,10 +279,10 @@ describe('table function overloads', () => {
217
279
  type MetadataColumn = typeof userTable.sqliteDef.columns.metadata
218
280
 
219
281
  // Should derive proper column schema
220
- expect((userTable.rowSchema as any).fields.age.toString()).toMatchInlineSnapshot(`"number"`)
282
+ expect((userTable.rowSchema as any).fields.age.toString()).toMatchInlineSnapshot(`"Int"`)
221
283
  expect((userTable.rowSchema as any).fields.active.toString()).toMatchInlineSnapshot(`"(number <-> boolean)"`)
222
284
  expect((userTable.rowSchema as any).fields.metadata.toString()).toMatchInlineSnapshot(
223
- `"(parseJson <-> { readonly [x: string]: unknown } | undefined)"`,
285
+ `"(parseJson <-> { readonly [x: string]: unknown }) | null"`,
224
286
  )
225
287
 
226
288
  // These should compile without errors
@@ -1,5 +1,5 @@
1
1
  import { type Nullable, shouldNeverHappen } from '@livestore/utils'
2
- import { Option, type Schema, SchemaAST, type Types } from '@livestore/utils/effect'
2
+ import { Option, Schema, SchemaAST, type Types } from '@livestore/utils/effect'
3
3
 
4
4
  import { getColumnDefForSchema, schemaFieldsToColumns } from './column-def.ts'
5
5
  import { SqliteDsl } from './db-schema/mod.ts'
@@ -221,7 +221,7 @@ export function table<
221
221
  ) as SqliteDsl.Columns
222
222
  additionalIndexes = []
223
223
  } else if ('schema' in args) {
224
- const result = schemaFieldsToColumns(SchemaAST.getPropertySignatures(args.schema.ast))
224
+ const result = schemaFieldsToColumns(Schema.getResolvedPropertySignatures(args.schema))
225
225
  columns = result.columns
226
226
 
227
227
  // We'll set tableName first, then use it for index names
@@ -381,12 +381,13 @@ export declare namespace SchemaToColumns {
381
381
  export type ColumnDefForType<TEncoded, TType> = SqliteDsl.ColumnDefinition<TEncoded, TType>
382
382
 
383
383
  // Create columns type from schema Type and Encoded
384
- export type FromTypes<TType, TEncoded> = TType extends Record<string, any>
385
- ? TEncoded extends Record<string, any>
386
- ? {
387
- [K in keyof TType & keyof TEncoded]: ColumnDefForType<TEncoded[K], TType[K]>
388
- }
389
- : SqliteDsl.Columns
384
+ export type FromTypes<TType, TEncoded> = TEncoded extends Record<string, any>
385
+ ? {
386
+ [K in keyof TEncoded]-?: ColumnDefForType<
387
+ TEncoded[K],
388
+ TType extends Record<string, any> ? (K extends keyof TType ? TType[K] : TEncoded[K]) : TEncoded[K]
389
+ >
390
+ }
390
391
  : SqliteDsl.Columns
391
392
  }
392
393
 
@@ -0,0 +1,131 @@
1
+ import { Effect } from '@livestore/utils/effect'
2
+
3
+ import { UnknownEventError } from '../errors.ts'
4
+ import type { EventDef, Materializer } from './EventDef.ts'
5
+ import type * as LiveStoreEvent from './LiveStoreEvent.ts'
6
+ import type { LiveStoreSchema } from './schema.ts'
7
+
8
+ export type UnknownEventContext = {
9
+ readonly event: Pick<LiveStoreEvent.AnyEncoded, 'name' | 'args' | 'seqNum' | 'clientId' | 'sessionId'>
10
+ readonly reason: 'event-definition-missing' | 'materializer-missing'
11
+ readonly operation: string
12
+ }
13
+
14
+ export namespace UnknownEvents {
15
+ export type HandlingStrategy = 'warn' | 'fail' | 'ignore' | 'callback'
16
+
17
+ export type Callback = (
18
+ context: UnknownEventContext,
19
+ error: UnknownEventError,
20
+ ) => Effect.SyncOrPromiseOrEffect<void, unknown, never>
21
+
22
+ export type HandlingConfig =
23
+ | { readonly strategy: 'warn' }
24
+ | { readonly strategy: 'ignore' }
25
+ | { readonly strategy: 'fail' }
26
+ | { readonly strategy: 'callback'; readonly onUnknownEvent: Callback }
27
+
28
+ export type Reason = UnknownEventContext['reason']
29
+
30
+ export type ResolveContext = Omit<UnknownEventContext, 'reason'>
31
+
32
+ export type Resolved =
33
+ | {
34
+ readonly _tag: 'known'
35
+ readonly eventDef: EventDef.AnyWithoutFn
36
+ readonly materializer: Materializer
37
+ }
38
+ | {
39
+ readonly _tag: 'unknown'
40
+ readonly reason: Reason
41
+ }
42
+ }
43
+
44
+ const DEFAULT_UNKNOWN_EVENT_HANDLING: UnknownEvents.HandlingConfig = { strategy: 'warn' }
45
+
46
+ export const normalizeUnknownEventHandling = (
47
+ input: UnknownEvents.HandlingConfig | undefined,
48
+ ): UnknownEvents.HandlingConfig => input ?? DEFAULT_UNKNOWN_EVENT_HANDLING
49
+
50
+ const handleUnknownEvent = ({
51
+ schema,
52
+ context,
53
+ }: {
54
+ schema: LiveStoreSchema
55
+ context: UnknownEventContext
56
+ }): Effect.Effect<void, UnknownEventError> =>
57
+ Effect.gen(function* () {
58
+ const config = schema.unknownEventHandling
59
+ const error = new UnknownEventError(context)
60
+
61
+ switch (config.strategy) {
62
+ case 'fail': {
63
+ return yield* Effect.fail(error)
64
+ }
65
+ case 'warn': {
66
+ yield* Effect.logWarning('@livestore/common:schema:unknown-event', context)
67
+ return
68
+ }
69
+ case 'ignore': {
70
+ return
71
+ }
72
+ case 'callback': {
73
+ const callback = config.onUnknownEvent
74
+
75
+ yield* Effect.tryAll<void>(() => callback(context, error)).pipe(
76
+ Effect.catchAll((cause) =>
77
+ Effect.logWarning('@livestore/common:schema:unknown-event:callback-error', {
78
+ event: context.event,
79
+ reason: context.reason,
80
+ operation: context.operation,
81
+ cause,
82
+ }),
83
+ ),
84
+ )
85
+ return
86
+ }
87
+ }
88
+ })
89
+
90
+ /**
91
+ * Resolves the runtime event definition + materializer for a given event name.
92
+ *
93
+ * Behaviour is intentionally split across the result and error channels:
94
+ * - For `'fail'` handling, we surface an `UnknownEventError` via the failure channel so
95
+ * callers can convert it into the appropriate domain error (for example `MaterializeError`).
96
+ * - For all other strategies (`warn`, `ignore`, `callback`) we succeed with an
97
+ * `{ _tag: 'unknown' }` value, signalling that the caller should skip the event while
98
+ * continuing normal processing.
99
+ */
100
+ export const resolveEventDef = (
101
+ schema: LiveStoreSchema,
102
+ context: UnknownEvents.ResolveContext,
103
+ ): Effect.Effect<UnknownEvents.Resolved, UnknownEventError> =>
104
+ Effect.gen(function* () {
105
+ const eventName = context.event.name
106
+ const eventDef = schema.eventsDefsMap.get(eventName)
107
+ if (eventDef === undefined) {
108
+ yield* handleUnknownEvent({
109
+ schema,
110
+ context: {
111
+ event: context.event,
112
+ reason: 'event-definition-missing',
113
+ operation: context.operation,
114
+ },
115
+ })
116
+ return { _tag: 'unknown', reason: 'event-definition-missing' }
117
+ }
118
+ const materializer = schema.state.materializers.get(eventName)
119
+ if (materializer === undefined) {
120
+ yield* handleUnknownEvent({
121
+ schema,
122
+ context: {
123
+ event: context.event,
124
+ reason: 'materializer-missing',
125
+ operation: context.operation,
126
+ },
127
+ })
128
+ return { _tag: 'unknown', reason: 'materializer-missing' }
129
+ }
130
+ return { _tag: 'known', eventDef, materializer }
131
+ })
@@ -1,3 +1,4 @@
1
+ import { omitUndefineds } from '@livestore/utils'
1
2
  import type { SqliteDsl } from '../schema/state/sqlite/db-schema/mod.ts'
2
3
  import type { BindValues } from './sql-queries.ts'
3
4
  import * as SqlQueries from './sql-queries.ts'
@@ -16,7 +17,7 @@ export const makeSqlQueryBuilder = <TSchema extends SqliteDsl.DbSchema>(schema:
16
17
  limit?: number
17
18
  }): [string, BindValues, TTableName] => {
18
19
  const columns = schema[tableName]!.columns
19
- const [stmt, bindValues] = SqlQueries.findManyRows({ columns, tableName, where, limit })
20
+ const [stmt, bindValues] = SqlQueries.findManyRows({ columns, tableName, where, ...omitUndefineds({ limit }) })
20
21
  return [stmt, bindValues, tableName]
21
22
  }
22
23