@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/ClientSessionLeaderThreadProxy.d.ts +7 -2
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
- package/dist/ClientSessionLeaderThreadProxy.js.map +1 -1
- package/dist/adapter-types.d.ts +9 -3
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js.map +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
- package/dist/devtools/devtools-messages-common.d.ts +7 -14
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.js +1 -6
- package/dist/devtools/devtools-messages-common.js.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +27 -25
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/errors.d.ts +47 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +22 -3
- package/dist/errors.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +7 -3
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +122 -49
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +4 -10
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +4 -6
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +6 -2
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +1 -2
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +68 -19
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.test.d.ts +2 -0
- package/dist/leader-thread/make-leader-thread-layer.test.d.ts.map +1 -0
- package/dist/leader-thread/make-leader-thread-layer.test.js +32 -0
- package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -0
- package/dist/leader-thread/materialize-event.d.ts +2 -2
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +23 -9
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +2 -3
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +1 -1
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.d.ts +2 -2
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +2 -2
- package/dist/leader-thread/shutdown-channel.js.map +1 -1
- package/dist/leader-thread/types.d.ts +7 -5
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/materializer-helper.d.ts +1 -1
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +20 -4
- package/dist/materializer-helper.js.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +1 -1
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.js +25 -16
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +3 -0
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js.map +1 -1
- package/dist/schema/LiveStoreEvent.d.ts +1 -1
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +1 -2
- package/dist/schema/LiveStoreEvent.js.map +1 -1
- package/dist/schema/mod.d.ts +2 -0
- package/dist/schema/mod.d.ts.map +1 -1
- package/dist/schema/mod.js +1 -0
- package/dist/schema/mod.js.map +1 -1
- package/dist/schema/schema.d.ts +15 -0
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +26 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.d.ts +35 -5
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js +95 -4
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +16 -0
- package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.js +14 -6
- package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.d.ts +6 -2
- package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-def.js +122 -185
- package/dist/schema/state/sqlite/column-def.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.test.js +116 -73
- package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +23 -6
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js +2 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
- package/dist/schema/state/sqlite/mod.d.ts +1 -1
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/mod.js +1 -1
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +137 -2
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables.js +2 -0
- package/dist/schema/state/sqlite/system-tables.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +4 -4
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.js +2 -2
- package/dist/schema/state/sqlite/table-def.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.test.js +51 -2
- package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
- package/dist/schema/unknown-events.d.ts +47 -0
- package/dist/schema/unknown-events.d.ts.map +1 -0
- package/dist/schema/unknown-events.js +69 -0
- package/dist/schema/unknown-events.js.map +1 -0
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sql-queries/sql-query-builder.js +2 -1
- package/dist/sql-queries/sql-query-builder.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -11
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +35 -33
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/errors.d.ts +61 -0
- package/dist/sync/errors.d.ts.map +1 -0
- package/dist/sync/errors.js +36 -0
- package/dist/sync/errors.js.map +1 -0
- package/dist/sync/index.d.ts +3 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +3 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/mock-sync-backend.d.ts +23 -0
- package/dist/sync/mock-sync-backend.d.ts.map +1 -0
- package/dist/sync/mock-sync-backend.js +114 -0
- package/dist/sync/mock-sync-backend.js.map +1 -0
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/compact-events.js +4 -5
- package/dist/sync/next/compact-events.js.map +1 -1
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +1 -2
- package/dist/sync/next/facts.js.map +1 -1
- package/dist/sync/next/history-dag-common.d.ts +50 -11
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +193 -4
- package/dist/sync/next/history-dag-common.js.map +1 -1
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/history-dag.js +3 -1
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/sync-backend-kv.d.ts +7 -0
- package/dist/sync/sync-backend-kv.d.ts.map +1 -0
- package/dist/sync/sync-backend-kv.js +18 -0
- package/dist/sync/sync-backend-kv.js.map +1 -0
- package/dist/sync/sync-backend.d.ts +105 -0
- package/dist/sync/sync-backend.d.ts.map +1 -0
- package/dist/sync/sync-backend.js +61 -0
- package/dist/sync/sync-backend.js.map +1 -0
- package/dist/sync/sync.d.ts +6 -84
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +2 -27
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/transport-chunking.d.ts +36 -0
- package/dist/sync/transport-chunking.d.ts.map +1 -0
- package/dist/sync/transport-chunking.js +56 -0
- package/dist/sync/transport-chunking.js.map +1 -0
- package/dist/sync/validate-push-payload.d.ts +1 -1
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +6 -6
- package/dist/sync/validate-push-payload.js.map +1 -1
- package/dist/testing/event-factory.d.ts +68 -0
- package/dist/testing/event-factory.d.ts.map +1 -0
- package/dist/testing/event-factory.js +80 -0
- package/dist/testing/event-factory.js.map +1 -0
- package/dist/testing/mod.d.ts +2 -0
- package/dist/testing/mod.d.ts.map +1 -0
- package/dist/testing/mod.js +2 -0
- package/dist/testing/mod.js.map +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +7 -8
- package/src/ClientSessionLeaderThreadProxy.ts +7 -2
- package/src/adapter-types.ts +13 -3
- package/src/devtools/devtools-messages-common.ts +1 -8
- package/src/errors.ts +33 -4
- package/src/leader-thread/LeaderSyncProcessor.ts +179 -57
- package/src/leader-thread/eventlog.ts +10 -6
- package/src/leader-thread/leader-worker-devtools.ts +6 -2
- package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
- package/src/leader-thread/make-leader-thread-layer.ts +137 -26
- package/src/leader-thread/materialize-event.ts +34 -9
- package/src/leader-thread/recreate-db.ts +11 -3
- package/src/leader-thread/shutdown-channel.ts +16 -2
- package/src/leader-thread/types.ts +7 -5
- package/src/materializer-helper.ts +22 -5
- package/src/rematerialize-from-eventlog.ts +33 -23
- package/src/schema/EventDef.ts +3 -0
- package/src/schema/LiveStoreEvent.ts +1 -2
- package/src/schema/mod.ts +2 -0
- package/src/schema/schema.ts +37 -1
- package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
- package/src/schema/state/sqlite/client-document-def.ts +117 -5
- package/src/schema/state/sqlite/column-annotations.ts +16 -6
- package/src/schema/state/sqlite/column-def.test.ts +150 -93
- package/src/schema/state/sqlite/column-def.ts +128 -203
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
- package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
- package/src/schema/state/sqlite/mod.ts +1 -0
- package/src/schema/state/sqlite/query-builder/api.ts +7 -2
- package/src/schema/state/sqlite/query-builder/impl.test.ts +187 -6
- package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
- package/src/schema/state/sqlite/system-tables.ts +2 -0
- package/src/schema/state/sqlite/table-def.test.ts +64 -2
- package/src/schema/state/sqlite/table-def.ts +9 -8
- package/src/schema/unknown-events.ts +131 -0
- package/src/sql-queries/sql-query-builder.ts +2 -1
- package/src/sync/ClientSessionSyncProcessor.ts +55 -49
- package/src/sync/errors.ts +38 -0
- package/src/sync/index.ts +3 -0
- package/src/sync/mock-sync-backend.ts +184 -0
- package/src/sync/next/compact-events.ts +4 -5
- package/src/sync/next/facts.ts +1 -3
- package/src/sync/next/history-dag-common.ts +272 -21
- package/src/sync/next/history-dag.ts +3 -1
- package/src/sync/sync-backend-kv.ts +22 -0
- package/src/sync/sync-backend.ts +185 -0
- package/src/sync/sync.ts +6 -89
- package/src/sync/transport-chunking.ts +90 -0
- package/src/sync/validate-push-payload.ts +6 -7
- package/src/testing/event-factory.ts +133 -0
- package/src/testing/mod.ts +1 -0
- package/src/version.ts +2 -2
- package/dist/schema-management/migrations.test.d.ts +0 -2
- package/dist/schema-management/migrations.test.d.ts.map +0 -1
- package/dist/schema-management/migrations.test.js +0 -52
- package/dist/schema-management/migrations.test.js.map +0 -1
- package/dist/sync/next/graphology.d.ts +0 -8
- package/dist/sync/next/graphology.d.ts.map +0 -1
- package/dist/sync/next/graphology.js +0 -30
- package/dist/sync/next/graphology.js.map +0 -1
- package/dist/sync/next/graphology_.d.ts +0 -3
- package/dist/sync/next/graphology_.d.ts.map +0 -1
- package/dist/sync/next/graphology_.js +0 -3
- package/dist/sync/next/graphology_.js.map +0 -1
- package/src/sync/next/ambient.d.ts +0 -3
- package/src/sync/next/graphology.ts +0 -41
- 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(`"
|
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 } |
|
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,
|
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(
|
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> =
|
385
|
-
?
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
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
|
|