@livestore/common 0.4.0-dev.0 → 0.4.0-dev.2
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/column-def.d.ts +19 -0
- package/dist/schema/state/sqlite/column-def.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-def.js +179 -0
- package/dist/schema/state/sqlite/column-def.js.map +1 -0
- package/dist/schema/state/sqlite/column-def.test.d.ts +2 -0
- package/dist/schema/state/sqlite/column-def.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-def.test.js +572 -0
- package/dist/schema/state/sqlite/column-def.test.js.map +1 -0
- package/dist/schema/state/sqlite/table-def.d.ts +3 -5
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.js +4 -211
- package/dist/schema/state/sqlite/table-def.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.test.js +15 -453
- 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/column-def.test.ts +722 -0
- package/src/schema/state/sqlite/column-def.ts +215 -0
- package/src/schema/state/sqlite/table-def.test.ts +21 -569
- package/src/schema/state/sqlite/table-def.ts +6 -256
- package/src/version.ts +1 -1
@@ -0,0 +1,215 @@
|
|
1
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
2
|
+
import { Option, Schema, SchemaAST } from '@livestore/utils/effect'
|
3
|
+
|
4
|
+
import { AutoIncrement, ColumnType, Default, PrimaryKeyId, Unique } from './column-annotations.ts'
|
5
|
+
import { SqliteDsl } from './db-schema/mod.ts'
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Maps a schema to a SQLite column definition, respecting column annotations.
|
9
|
+
*
|
10
|
+
* Note: When used with schema-based table definitions, optional fields (| undefined)
|
11
|
+
* are transformed to nullable fields (| null) to match SQLite's NULL semantics.
|
12
|
+
* Fields with both null and undefined will emit a warning as this is a lossy conversion.
|
13
|
+
*/
|
14
|
+
export const getColumnDefForSchema = (
|
15
|
+
schema: Schema.Schema.AnyNoContext,
|
16
|
+
propertySignature?: SchemaAST.PropertySignature,
|
17
|
+
forceNullable = false,
|
18
|
+
): SqliteDsl.ColumnDefinition.Any => {
|
19
|
+
const ast = schema.ast
|
20
|
+
|
21
|
+
// Extract annotations
|
22
|
+
const getAnnotation = <T>(annotationId: symbol): Option.Option<T> =>
|
23
|
+
propertySignature
|
24
|
+
? hasPropertyAnnotation<T>(propertySignature, annotationId)
|
25
|
+
: SchemaAST.getAnnotation<T>(annotationId)(ast)
|
26
|
+
|
27
|
+
const columnType = SchemaAST.getAnnotation<SqliteDsl.FieldColumnType>(ColumnType)(ast)
|
28
|
+
|
29
|
+
// Check if schema has null (e.g., Schema.NullOr) or undefined or if it's forced nullable (optional field)
|
30
|
+
const isNullable = forceNullable || hasNull(ast) || hasUndefined(ast)
|
31
|
+
|
32
|
+
// Get base column definition with nullable flag
|
33
|
+
const baseColumn = Option.isSome(columnType)
|
34
|
+
? getColumnForType(columnType.value, isNullable)
|
35
|
+
: getColumnForSchema(schema, isNullable)
|
36
|
+
|
37
|
+
// Apply annotations
|
38
|
+
const primaryKey = getAnnotation<boolean>(PrimaryKeyId).pipe(Option.getOrElse(() => false))
|
39
|
+
const autoIncrement = getAnnotation<boolean>(AutoIncrement).pipe(Option.getOrElse(() => false))
|
40
|
+
const defaultValue = getAnnotation<unknown>(Default)
|
41
|
+
|
42
|
+
return {
|
43
|
+
...baseColumn,
|
44
|
+
...(primaryKey && { primaryKey: true }),
|
45
|
+
...(autoIncrement && { autoIncrement: true }),
|
46
|
+
...(Option.isSome(defaultValue) && { default: Option.some(defaultValue.value) }),
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
const hasPropertyAnnotation = <T>(
|
51
|
+
propertySignature: SchemaAST.PropertySignature,
|
52
|
+
annotationId: symbol,
|
53
|
+
): Option.Option<T> => {
|
54
|
+
if ('annotations' in propertySignature && propertySignature.annotations) {
|
55
|
+
const annotation = SchemaAST.getAnnotation<T>(annotationId)(propertySignature as any)
|
56
|
+
if (Option.isSome(annotation)) return annotation
|
57
|
+
}
|
58
|
+
return SchemaAST.getAnnotation<T>(annotationId)(propertySignature.type)
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Maps schema property signatures to SQLite column definitions.
|
63
|
+
* Optional fields (| undefined) become nullable columns (| null).
|
64
|
+
*/
|
65
|
+
export const schemaFieldsToColumns = (
|
66
|
+
propertySignatures: ReadonlyArray<SchemaAST.PropertySignature>,
|
67
|
+
): { columns: SqliteDsl.Columns; uniqueColumns: string[] } => {
|
68
|
+
const columns: SqliteDsl.Columns = {}
|
69
|
+
const uniqueColumns: string[] = []
|
70
|
+
|
71
|
+
for (const prop of propertySignatures) {
|
72
|
+
if (typeof prop.name !== 'string') continue
|
73
|
+
|
74
|
+
const fieldSchema = Schema.make(prop.type)
|
75
|
+
|
76
|
+
// Warn about lossy conversion for fields with both null and undefined
|
77
|
+
if (prop.isOptional) {
|
78
|
+
const { hasNull, hasUndefined } = checkNullUndefined(fieldSchema.ast)
|
79
|
+
if (hasNull && hasUndefined) {
|
80
|
+
console.warn(`Field '${prop.name}' has both null and undefined - treating | undefined as | null`)
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
// Get column definition - pass nullable flag for optional fields
|
85
|
+
const columnDef = getColumnDefForSchema(fieldSchema, prop, prop.isOptional)
|
86
|
+
|
87
|
+
// Check for primary key and unique annotations
|
88
|
+
const hasPrimaryKey = hasPropertyAnnotation<boolean>(prop, PrimaryKeyId).pipe(Option.getOrElse(() => false))
|
89
|
+
const hasUnique = hasPropertyAnnotation<boolean>(prop, Unique).pipe(Option.getOrElse(() => false))
|
90
|
+
|
91
|
+
// Build final column
|
92
|
+
columns[prop.name] = {
|
93
|
+
...columnDef,
|
94
|
+
...(hasPrimaryKey && { primaryKey: true }),
|
95
|
+
}
|
96
|
+
|
97
|
+
// Validate primary key + nullable
|
98
|
+
const column = columns[prop.name]
|
99
|
+
if (column?.primaryKey && column.nullable) {
|
100
|
+
throw new Error('Primary key columns cannot be nullable')
|
101
|
+
}
|
102
|
+
|
103
|
+
if (hasUnique) uniqueColumns.push(prop.name)
|
104
|
+
}
|
105
|
+
|
106
|
+
return { columns, uniqueColumns }
|
107
|
+
}
|
108
|
+
|
109
|
+
const checkNullUndefined = (ast: SchemaAST.AST): { hasNull: boolean; hasUndefined: boolean } => {
|
110
|
+
let hasNull = false
|
111
|
+
let hasUndefined = false
|
112
|
+
|
113
|
+
const visit = (type: SchemaAST.AST): void => {
|
114
|
+
if (SchemaAST.isUndefinedKeyword(type)) hasUndefined = true
|
115
|
+
else if (SchemaAST.isLiteral(type) && type.literal === null) hasNull = true
|
116
|
+
else if (SchemaAST.isUnion(type)) type.types.forEach(visit)
|
117
|
+
}
|
118
|
+
|
119
|
+
visit(ast)
|
120
|
+
return { hasNull, hasUndefined }
|
121
|
+
}
|
122
|
+
|
123
|
+
const hasNull = (ast: SchemaAST.AST): boolean => {
|
124
|
+
if (SchemaAST.isLiteral(ast) && ast.literal === null) return true
|
125
|
+
if (SchemaAST.isUnion(ast)) {
|
126
|
+
return ast.types.some((type) => hasNull(type))
|
127
|
+
}
|
128
|
+
return false
|
129
|
+
}
|
130
|
+
|
131
|
+
const hasUndefined = (ast: SchemaAST.AST): boolean => {
|
132
|
+
if (SchemaAST.isUndefinedKeyword(ast)) return true
|
133
|
+
if (SchemaAST.isUnion(ast)) {
|
134
|
+
return ast.types.some((type) => hasUndefined(type))
|
135
|
+
}
|
136
|
+
return false
|
137
|
+
}
|
138
|
+
|
139
|
+
const getColumnForType = (columnType: string, nullable = false): SqliteDsl.ColumnDefinition.Any => {
|
140
|
+
switch (columnType) {
|
141
|
+
case 'text':
|
142
|
+
return SqliteDsl.text({ nullable })
|
143
|
+
case 'integer':
|
144
|
+
return SqliteDsl.integer({ nullable })
|
145
|
+
case 'real':
|
146
|
+
return SqliteDsl.real({ nullable })
|
147
|
+
case 'blob':
|
148
|
+
return SqliteDsl.blob({ nullable })
|
149
|
+
default:
|
150
|
+
return shouldNeverHappen(`Unsupported column type: ${columnType}`)
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
const getColumnForSchema = (schema: Schema.Schema.AnyNoContext, nullable = false): SqliteDsl.ColumnDefinition.Any => {
|
155
|
+
const ast = schema.ast
|
156
|
+
// Strip nullable wrapper to get core type
|
157
|
+
const coreAst = stripNullable(ast)
|
158
|
+
const coreSchema = stripNullable(ast) === ast ? schema : Schema.make(coreAst)
|
159
|
+
|
160
|
+
// Special case: Boolean is transformed to integer in SQLite
|
161
|
+
if (SchemaAST.isBooleanKeyword(coreAst)) {
|
162
|
+
return SqliteDsl.boolean({ nullable })
|
163
|
+
}
|
164
|
+
|
165
|
+
// Get the encoded AST - what actually gets stored in SQLite
|
166
|
+
const encodedAst = Schema.encodedSchema(coreSchema).ast
|
167
|
+
|
168
|
+
// Check if the encoded type matches SQLite native types
|
169
|
+
if (SchemaAST.isStringKeyword(encodedAst)) {
|
170
|
+
return SqliteDsl.text({ schema: coreSchema, nullable })
|
171
|
+
}
|
172
|
+
|
173
|
+
if (SchemaAST.isNumberKeyword(encodedAst)) {
|
174
|
+
// Special cases for integer columns
|
175
|
+
const id = SchemaAST.getIdentifierAnnotation(coreAst).pipe(Option.getOrElse(() => ''))
|
176
|
+
if (id === 'Int' || id === 'DateFromNumber') {
|
177
|
+
return SqliteDsl.integer({ schema: coreSchema, nullable })
|
178
|
+
}
|
179
|
+
return SqliteDsl.real({ schema: coreSchema, nullable })
|
180
|
+
}
|
181
|
+
|
182
|
+
// Literals based on their type
|
183
|
+
if (SchemaAST.isLiteral(coreAst)) {
|
184
|
+
const value = coreAst.literal
|
185
|
+
if (typeof value === 'boolean') return SqliteDsl.boolean({ nullable })
|
186
|
+
}
|
187
|
+
|
188
|
+
// Literals based on their encoded type
|
189
|
+
if (SchemaAST.isLiteral(encodedAst)) {
|
190
|
+
const value = encodedAst.literal
|
191
|
+
if (typeof value === 'string') return SqliteDsl.text({ schema: coreSchema, nullable })
|
192
|
+
if (typeof value === 'number') {
|
193
|
+
// Check if the original schema is Int
|
194
|
+
const id = SchemaAST.getIdentifierAnnotation(coreAst).pipe(Option.getOrElse(() => ''))
|
195
|
+
if (id === 'Int') {
|
196
|
+
return SqliteDsl.integer({ schema: coreSchema, nullable })
|
197
|
+
}
|
198
|
+
return SqliteDsl.real({ schema: coreSchema, nullable })
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
// Everything else needs JSON encoding
|
203
|
+
return SqliteDsl.json({ schema: coreSchema, nullable })
|
204
|
+
}
|
205
|
+
|
206
|
+
const stripNullable = (ast: SchemaAST.AST): SchemaAST.AST => {
|
207
|
+
if (!SchemaAST.isUnion(ast)) return ast
|
208
|
+
|
209
|
+
// Find non-null/undefined type
|
210
|
+
const core = ast.types.find(
|
211
|
+
(type) => !(SchemaAST.isLiteral(type) && type.literal === null) && !SchemaAST.isUndefinedKeyword(type),
|
212
|
+
)
|
213
|
+
|
214
|
+
return core || ast
|
215
|
+
}
|