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

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 (29) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  3. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  4. package/dist/devtools/devtools-messages-leader.d.ts +24 -24
  5. package/dist/schema/state/sqlite/client-document-def.d.ts +5 -3
  6. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  7. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  8. package/dist/schema/state/sqlite/client-document-def.test.js +15 -0
  9. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  10. package/dist/schema/state/sqlite/column-def.d.ts +6 -2
  11. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
  12. package/dist/schema/state/sqlite/column-def.js +122 -185
  13. package/dist/schema/state/sqlite/column-def.js.map +1 -1
  14. package/dist/schema/state/sqlite/column-def.test.js +116 -73
  15. package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
  16. package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
  17. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  18. package/dist/schema/state/sqlite/table-def.test.js +7 -2
  19. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  20. package/dist/version.d.ts +1 -1
  21. package/dist/version.js +1 -1
  22. package/package.json +4 -4
  23. package/src/schema/state/sqlite/client-document-def.test.ts +16 -0
  24. package/src/schema/state/sqlite/client-document-def.ts +7 -1
  25. package/src/schema/state/sqlite/column-def.test.ts +150 -93
  26. package/src/schema/state/sqlite/column-def.ts +128 -203
  27. package/src/schema/state/sqlite/query-builder/api.ts +7 -2
  28. package/src/schema/state/sqlite/table-def.test.ts +11 -2
  29. package/src/version.ts +1 -1
@@ -24,6 +24,15 @@ describe('getColumnDefForSchema', () => {
24
24
  it('should map Schema.Date to text column', () => {
25
25
  const columnDef = State.SQLite.getColumnDefForSchema(Schema.Date)
26
26
  expect(columnDef.columnType).toBe('text')
27
+ expect(Schema.encodedSchema(columnDef.schema).toString()).toBe('string')
28
+ expect(Schema.typeSchema(columnDef.schema).toString()).toBe('Date')
29
+ })
30
+
31
+ it('should map Schema.DateFromNumber to integer column', () => {
32
+ const columnDef = State.SQLite.getColumnDefForSchema(Schema.DateFromNumber)
33
+ expect(columnDef.columnType).toBe('integer')
34
+ expect(Schema.encodedSchema(columnDef.schema).toString()).toBe('number')
35
+ expect(Schema.typeSchema(columnDef.schema).toString()).toBe('DateFromSelf')
27
36
  })
28
37
 
29
38
  it('should map Schema.BigInt to text column', () => {
@@ -98,7 +107,7 @@ describe('getColumnDefForSchema', () => {
98
107
  )
99
108
 
100
109
  const columnDef = State.SQLite.getColumnDefForSchema(StringToNumber)
101
- expect(columnDef.columnType).toBe('real') // Based on the target type (Number)
110
+ expect(columnDef.columnType).toBe('text') // Based on the encoded type (String)
102
111
  })
103
112
 
104
113
  it('should handle Date transformations', () => {
@@ -294,6 +303,145 @@ describe('getColumnDefForSchema', () => {
294
303
  })
295
304
  })
296
305
 
306
+ describe('schema-based table definitions', () => {
307
+ it('should handle optional fields in schema', () => {
308
+ const UserSchema = Schema.Struct({
309
+ id: Schema.String,
310
+ name: Schema.String,
311
+ email: Schema.optional(Schema.String),
312
+ age: Schema.optional(Schema.Number),
313
+ })
314
+
315
+ const userTable = State.SQLite.table({
316
+ name: 'users',
317
+ schema: UserSchema,
318
+ })
319
+
320
+ // Optional fields should be nullable
321
+ expect(userTable.sqliteDef.columns.email.nullable).toBe(true)
322
+ expect(userTable.sqliteDef.columns.age.nullable).toBe(true)
323
+
324
+ // Non-optional fields should not be nullable
325
+ expect(userTable.sqliteDef.columns.id.nullable).toBe(false)
326
+ expect(userTable.sqliteDef.columns.name.nullable).toBe(false)
327
+
328
+ // Row schema should show | null for optional fields
329
+ expect((userTable.rowSchema as any).fields.email.toString()).toBe('string | null')
330
+ expect((userTable.rowSchema as any).fields.age.toString()).toBe('number | null')
331
+ })
332
+
333
+ it('should handle optional boolean with proper transformation', () => {
334
+ const schema = Schema.Struct({
335
+ id: Schema.String,
336
+ active: Schema.optional(Schema.Boolean),
337
+ })
338
+
339
+ const table = State.SQLite.table({ name: 'test', schema })
340
+
341
+ expect(table.sqliteDef.columns.active.nullable).toBe(true)
342
+ expect(table.sqliteDef.columns.active.columnType).toBe('integer')
343
+ expect(table.sqliteDef.columns.active.schema.toString()).toBe('(number <-> boolean) | null')
344
+ expect((table.rowSchema as any).fields.active.toString()).toBe('(number <-> boolean) | null')
345
+ })
346
+
347
+ it('should handle optional complex types with JSON encoding', () => {
348
+ const schema = Schema.Struct({
349
+ id: Schema.String,
350
+ metadata: Schema.optional(Schema.Struct({ color: Schema.String })),
351
+ tags: Schema.optional(Schema.Array(Schema.String)),
352
+ })
353
+
354
+ const table = State.SQLite.table({ name: 'test', schema })
355
+
356
+ expect(table.sqliteDef.columns.metadata.nullable).toBe(true)
357
+ expect(table.sqliteDef.columns.metadata.columnType).toBe('text')
358
+ expect((table.rowSchema as any).fields.metadata.toString()).toBe(
359
+ '(parseJson <-> { readonly color: string }) | null',
360
+ // '(parseJson <-> { readonly color: string } | null)', // not sure yet about which semantics we want here
361
+ )
362
+
363
+ expect(table.sqliteDef.columns.tags.nullable).toBe(true)
364
+ expect(table.sqliteDef.columns.tags.columnType).toBe('text')
365
+ expect((table.rowSchema as any).fields.tags.toString()).toBe('(parseJson <-> ReadonlyArray<string>) | null')
366
+ })
367
+
368
+ it('should handle Schema.NullOr', () => {
369
+ const schema = Schema.Struct({
370
+ id: Schema.String,
371
+ description: Schema.NullOr(Schema.String),
372
+ count: Schema.NullOr(Schema.Int),
373
+ })
374
+
375
+ const table = State.SQLite.table({ name: 'test', schema })
376
+
377
+ expect(table.sqliteDef.columns.description.nullable).toBe(true)
378
+ expect(table.sqliteDef.columns.count.nullable).toBe(true)
379
+
380
+ expect((table.rowSchema as any).fields.description.toString()).toBe('string | null')
381
+ expect((table.rowSchema as any).fields.count.toString()).toBe('Int | null')
382
+ })
383
+
384
+ it('should handle Schema.NullOr with complex types', () => {
385
+ const schema = Schema.Struct({
386
+ data: Schema.NullOr(Schema.Struct({ value: Schema.Number })),
387
+ }).annotations({ title: 'test' })
388
+
389
+ const table = State.SQLite.table({ schema })
390
+
391
+ expect(table.sqliteDef.columns.data.nullable).toBe(true)
392
+ expect(table.sqliteDef.columns.data.columnType).toBe('text')
393
+ expect((table.rowSchema as any).fields.data.toString()).toBe('(parseJson <-> { readonly value: number }) | null')
394
+ })
395
+
396
+ it('should handle mixed nullable and optional fields', () => {
397
+ const schema = Schema.Struct({
398
+ nullableText: Schema.NullOr(Schema.String),
399
+ optionalText: Schema.optional(Schema.String),
400
+ optionalJson: Schema.optional(Schema.Struct({ x: Schema.Number })),
401
+ }).annotations({ title: 'test' })
402
+
403
+ const table = State.SQLite.table({ schema })
404
+
405
+ // Both should be nullable at column level
406
+ expect(table.sqliteDef.columns.nullableText.nullable).toBe(true)
407
+ expect(table.sqliteDef.columns.optionalText.nullable).toBe(true)
408
+ expect(table.sqliteDef.columns.optionalJson.nullable).toBe(true)
409
+
410
+ // Schema representations
411
+ expect((table.rowSchema as any).fields.nullableText.toString()).toBe('string | null')
412
+ expect((table.rowSchema as any).fields.optionalText.toString()).toBe('string | null')
413
+ expect((table.rowSchema as any).fields.optionalJson.toString()).toBe(
414
+ '(parseJson <-> { readonly x: number }) | null',
415
+ )
416
+ })
417
+
418
+ // TODO bring back some time later
419
+ // it('should handle lossy Schema.optional(Schema.NullOr(...)) with JSON encoding', () => {
420
+ // const schema = Schema.Struct({
421
+ // id: Schema.String,
422
+ // lossyText: Schema.optional(Schema.NullOr(Schema.String)),
423
+ // lossyComplex: Schema.optional(Schema.NullOr(Schema.Struct({ value: Schema.Number }))),
424
+ // }).annotations({ title: 'lossy_test' })
425
+
426
+ // const table = State.SQLite.table({ schema })
427
+
428
+ // // Check column definitions for lossy fields
429
+ // expect(table.sqliteDef.columns.lossyText.nullable).toBe(true)
430
+ // expect(table.sqliteDef.columns.lossyText.columnType).toBe('text')
431
+ // expect(table.sqliteDef.columns.lossyComplex.nullable).toBe(true)
432
+ // expect(table.sqliteDef.columns.lossyComplex.columnType).toBe('text')
433
+
434
+ // // Check schema representations - should use parseJson for lossy encoding
435
+ // expect((table.rowSchema as any).fields.lossyText.toString()).toBe('(parseJson <-> string | null)')
436
+ // expect((table.rowSchema as any).fields.lossyComplex.toString()).toBe(
437
+ // '(parseJson <-> { readonly value: number } | null)',
438
+ // )
439
+
440
+ // // Note: Since we're converting undefined to null, this is a lossy transformation.
441
+ // // The test now just verifies that the schemas are set up correctly for JSON encoding.
442
+ // })
443
+ })
444
+
297
445
  describe('annotations', () => {
298
446
  describe('withColumnType', () => {
299
447
  it('should respect column type annotation for text', () => {
@@ -345,11 +493,6 @@ describe('getColumnDefForSchema', () => {
345
493
  const UserSchema = Schema.Struct({
346
494
  id: Schema.String.pipe(withPrimaryKey),
347
495
  name: Schema.String,
348
- email: Schema.optional(Schema.String),
349
- nullable: Schema.NullOr(Schema.Int),
350
- optionalComplex: Schema.optional(Schema.Struct({ color: Schema.String })),
351
- optionalNullableText: Schema.optional(Schema.NullOr(Schema.String)),
352
- optionalNullableComplex: Schema.optional(Schema.NullOr(Schema.Struct({ color: Schema.String }))),
353
496
  })
354
497
 
355
498
  const userTable = State.SQLite.table({
@@ -360,93 +503,7 @@ describe('getColumnDefForSchema', () => {
360
503
  expect(userTable.sqliteDef.columns.id.primaryKey).toBe(true)
361
504
  expect(userTable.sqliteDef.columns.id.nullable).toBe(false)
362
505
  expect(userTable.sqliteDef.columns.name.primaryKey).toBe(false)
363
- expect(userTable.sqliteDef.columns.email.primaryKey).toBe(false)
364
- expect(userTable.sqliteDef.columns.email.nullable).toBe(true)
365
- expect(userTable.sqliteDef.columns.nullable.primaryKey).toBe(false)
366
- expect(userTable.sqliteDef.columns.nullable.nullable).toBe(true)
367
- expect(userTable.sqliteDef.columns.optionalComplex.nullable).toBe(true)
368
- expect((userTable.rowSchema as any).fields.email.toString()).toBe('string | undefined')
369
- expect((userTable.rowSchema as any).fields.nullable.toString()).toBe('Int | null')
370
- expect((userTable.rowSchema as any).fields.optionalComplex.toString()).toBe(
371
- '(parseJson <-> { readonly color: string } | undefined)',
372
- )
373
- })
374
-
375
- it('should handle Schema.NullOr with complex types', () => {
376
- const schema = Schema.Struct({
377
- data: Schema.NullOr(Schema.Struct({ value: Schema.Number })),
378
- }).annotations({ title: 'test' })
379
-
380
- const table = State.SQLite.table({ schema })
381
-
382
- expect(table.sqliteDef.columns.data.nullable).toBe(true)
383
- expect(table.sqliteDef.columns.data.columnType).toBe('text')
384
- expect((table.rowSchema as any).fields.data.toString()).toBe('{ readonly value: number } | null')
385
- })
386
-
387
- it('should handle mixed nullable and optional fields', () => {
388
- const schema = Schema.Struct({
389
- nullableText: Schema.NullOr(Schema.String),
390
- optionalText: Schema.optional(Schema.String),
391
- optionalJson: Schema.optional(Schema.Struct({ x: Schema.Number })),
392
- }).annotations({ title: 'test' })
393
-
394
- const table = State.SQLite.table({ schema })
395
-
396
- // Both should be nullable at column level
397
- expect(table.sqliteDef.columns.nullableText.nullable).toBe(true)
398
- expect(table.sqliteDef.columns.optionalText.nullable).toBe(true)
399
- expect(table.sqliteDef.columns.optionalJson.nullable).toBe(true)
400
-
401
- // But different schema representations
402
- expect((table.rowSchema as any).fields.nullableText.toString()).toBe('string | null')
403
- expect((table.rowSchema as any).fields.optionalText.toString()).toBe('string | undefined')
404
- expect((table.rowSchema as any).fields.optionalJson.toString()).toBe(
405
- '(parseJson <-> { readonly x: number } | undefined)',
406
- )
407
- })
408
-
409
- it('should handle lossy Schema.optional(Schema.NullOr(...)) with JSON encoding', () => {
410
- const schema = Schema.Struct({
411
- id: Schema.String,
412
- lossyText: Schema.optional(Schema.NullOr(Schema.String)),
413
- lossyComplex: Schema.optional(Schema.NullOr(Schema.Struct({ value: Schema.Number }))),
414
- }).annotations({ title: 'lossy_test' })
415
-
416
- const table = State.SQLite.table({ schema })
417
-
418
- // Check column definitions for lossy fields
419
- expect(table.sqliteDef.columns.lossyText.nullable).toBe(true)
420
- expect(table.sqliteDef.columns.lossyText.columnType).toBe('text')
421
- expect(table.sqliteDef.columns.lossyComplex.nullable).toBe(true)
422
- expect(table.sqliteDef.columns.lossyComplex.columnType).toBe('text')
423
-
424
- // Check schema representations - should use parseJson for lossless encoding
425
- expect((table.rowSchema as any).fields.lossyText.toString()).toBe('(parseJson <-> string | null | undefined)')
426
- expect((table.rowSchema as any).fields.lossyComplex.toString()).toBe(
427
- '(parseJson <-> { readonly value: number } | null | undefined)',
428
- )
429
-
430
- // Test actual data round-tripping to ensure losslessness
431
- // Note: Missing field case is challenging with current Effect Schema design
432
- // as optional fields are handled at struct level, not field level
433
- const testCases = [
434
- // For now, test only cases where both lossy fields are present
435
- { name: 'both explicit null', data: { id: '2', lossyText: null, lossyComplex: null } },
436
- { name: 'text value, complex null', data: { id: '3', lossyText: 'hello', lossyComplex: null } },
437
- { name: 'text null, complex value', data: { id: '4', lossyText: null, lossyComplex: { value: 42 } } },
438
- { name: 'both values', data: { id: '5', lossyText: 'world', lossyComplex: { value: 42 } } },
439
- ]
440
-
441
- testCases.forEach((testCase) => {
442
- // Encode through insert schema
443
- const encoded = Schema.encodeSync(table.insertSchema)(testCase.data)
444
- // Decode through row schema
445
- const decoded = Schema.decodeSync(table.rowSchema)(encoded)
446
-
447
- // Check for losslessness
448
- expect(decoded).toEqual(testCase.data)
449
- })
506
+ expect(userTable.sqliteDef.columns.name.nullable).toBe(false)
450
507
  })
451
508
 
452
509
  it('should throw when primary key is used with optional schema', () => {