@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
- package/dist/devtools/devtools-messages-common.d.ts +6 -6
- package/dist/devtools/devtools-messages-leader.d.ts +24 -24
- package/dist/schema/state/sqlite/client-document-def.d.ts +5 -3
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +15 -0
- package/dist/schema/state/sqlite/client-document-def.test.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/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/table-def.test.js +7 -2
- package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/src/schema/state/sqlite/client-document-def.test.ts +16 -0
- package/src/schema/state/sqlite/client-document-def.ts +7 -1
- 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/query-builder/api.ts +7 -2
- package/src/schema/state/sqlite/table-def.test.ts +11 -2
- 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('
|
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.
|
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', () => {
|