@evolu/common 7.4.0 → 8.0.0-next.0

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 (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +1064 -630
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -823
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +209 -282
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -529
  246. package/src/Instances.ts +0 -90
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -960
@@ -1,86 +1,79 @@
1
+ /**
2
+ * Database schema definition and validation.
3
+ *
4
+ * @module
5
+ */
6
+
1
7
  import * as Kysely from "kysely";
8
+ import { getProperty, mapObject, type ReadonlyRecord } from "../Object.js";
2
9
  import {
3
- createRecord,
4
- getProperty,
5
- mapObject,
6
- ReadonlyRecord,
7
- } from "../Object.js";
8
- import { ok, Result } from "../Result.js";
9
- import {
10
- SafeSql,
10
+ eqSqliteIndex,
11
+ getSqliteSchema,
12
+ type SafeSql,
11
13
  sql,
14
+ sqliteQueryToSqliteQueryString,
12
15
  SqliteBoolean,
13
- SqliteDep,
14
- SqliteError,
15
- SqliteQuery,
16
- SqliteQueryOptions,
16
+ type SqliteDep,
17
+ type SqliteIndex,
18
+ type SqliteQuery,
19
+ type SqliteQueryOptions,
20
+ type SqliteSchema,
17
21
  SqliteValue,
18
22
  } from "../Sqlite.js";
23
+ import type { InferType } from "../Type.js";
19
24
  import {
20
- AnyType,
21
- array,
22
- createIdFromString,
23
25
  DateIso,
26
+ type Id,
24
27
  IdBytes,
25
- InferErrors,
26
- InferInput,
27
- InferType,
28
- maxMutationSize,
29
- MergeObjectTypeErrors,
30
- nullableToOptional,
31
- NullableToOptionalProps,
32
28
  nullOr,
33
29
  object,
34
- ObjectType,
35
- omit,
36
- optional,
37
- OptionalType,
38
- record,
39
- set,
40
- String,
41
- TableId,
42
- Type,
43
- ValidMutationSize,
44
- validMutationSize,
45
- ValidMutationSizeError,
30
+ type StandardSchemaV1,
46
31
  } from "../Type.js";
47
- import { Simplify } from "../Types.js";
48
- import { AppOwner, OwnerId } from "./Owner.js";
49
- import { Query, Row } from "./Query.js";
32
+ import type { Simplify } from "../Types.js";
33
+ import type { AppOwner } from "./Owner.js";
34
+ import { OwnerId } from "./Owner.js";
35
+ import type {
36
+ evoluJsonArrayFrom,
37
+ evoluJsonObjectFrom,
38
+ Query,
39
+ Row,
40
+ } from "./Query.js";
50
41
  import type { CrdtMessage, DbChange } from "./Storage.js";
51
- import { Timestamp, TimestampBytes } from "./Timestamp.js";
52
- import { readonly } from "../Function.js";
42
+ import { TimestampBytes } from "./Timestamp.js";
43
+
44
+ /** Any {@link StandardSchemaV1}. */
45
+ export type AnyStandardSchemaV1 = StandardSchemaV1<any, any>;
53
46
 
54
47
  /**
55
48
  * Defines the schema of an Evolu database.
56
49
  *
57
- * Table schema defines columns that are required for table rows. For not
58
- * required columns, use {@link nullOr}.
50
+ * Column types are Standard Schema v1 compatible use Evolu Type, Zod,
51
+ * Valibot, ArkType, or any library that implements Standard Schema.
52
+ *
53
+ * Table schema defines columns that are required for table rows. For optional
54
+ * columns, use a schema whose output type includes `null`.
59
55
  *
60
56
  * ### Example
61
57
  *
62
58
  * ```ts
59
+ * // With Evolu Type
63
60
  * const TodoId = id("Todo");
64
61
  * type TodoId = typeof TodoId.Type;
65
62
  *
66
- * const TodoCategoryId = id("TodoCategory");
67
- * type TodoCategoryId = typeof TodoCategoryId.Type;
68
- *
69
- * const NonEmptyString50 = maxLength(50)(NonEmptyString);
70
- * type NonEmptyString50 = typeof NonEmptyString50.Type;
71
- *
72
- * // Database schema.
73
63
  * const Schema = {
74
64
  * todo: {
75
65
  * id: TodoId,
76
- * title: NonEmptyString1000,
77
- * isCompleted: nullable(SqliteBoolean),
78
- * categoryId: nullable(TodoCategoryId),
66
+ * title: NonEmptyString100,
67
+ * isCompleted: nullOr(SqliteBoolean),
79
68
  * },
80
- * todoCategory: {
81
- * id: TodoCategoryId,
82
- * name: NonEmptyString50,
83
- * json: nullable(SomeJson),
69
+ * };
70
+ *
71
+ * // With Zod (or any Standard Schema library)
72
+ * const Schema = {
73
+ * todo: {
74
+ * id: TodoId, // Evolu id() for branded IDs
75
+ * title: z.string().min(1).max(100),
76
+ * isCompleted: z.union([z.literal(0), z.literal(1)]).nullable(),
84
77
  * },
85
78
  * };
86
79
  * ```
@@ -88,9 +81,16 @@ import { readonly } from "../Function.js";
88
81
  export type EvoluSchema = ReadonlyRecord<
89
82
  string,
90
83
  // TypeScript errors are cryptic so we use ValidateSchema.
91
- ReadonlyRecord<string, Type<any, any, any, any, any, any>>
84
+ TableSchema
92
85
  >;
93
86
 
87
+ /** A table schema: column names mapped to Standard Schema validators. */
88
+ export type TableSchema = ReadonlyRecord<string, AnyStandardSchemaV1>;
89
+
90
+ export interface SqliteSchemaDep {
91
+ readonly sqliteSchema: SqliteSchema;
92
+ }
93
+
94
94
  /**
95
95
  * Validates an {@link EvoluSchema} at compile time, returning the first error
96
96
  * found as a readable string literal type. This approach provides much clearer
@@ -100,9 +100,9 @@ export type EvoluSchema = ReadonlyRecord<
100
100
  * Validates the following schema requirements:
101
101
  *
102
102
  * 1. All tables must have an 'id' column
103
- * 2. The 'id' column must be a branded ID type (created with id() function)
103
+ * 2. The 'id' column output type must extend {@link Id}
104
104
  * 3. Tables cannot use system column names (createdAt, updatedAt, isDeleted)
105
- * 4. All column types must be compatible with SQLite (extend SqliteValue)
105
+ * 4. All column output types must be compatible with SQLite (extend SqliteValue)
106
106
  */
107
107
  export type ValidateSchema<S extends EvoluSchema> =
108
108
  ValidateSchemaHasId<S> extends never
@@ -115,85 +115,10 @@ export type ValidateSchema<S extends EvoluSchema> =
115
115
  : ValidateIdColumnType<S>
116
116
  : ValidateSchemaHasId<S>;
117
117
 
118
- export type ValidateSchemaHasId<S extends EvoluSchema> =
119
- keyof S extends infer TableName
120
- ? TableName extends keyof S
121
- ? "id" extends keyof S[TableName]
122
- ? never
123
- : SchemaValidationError<`Table "${TableName & string}" is missing required id column.`>
124
- : never
125
- : never;
126
-
127
- export type ValidateIdColumnType<S extends EvoluSchema> =
128
- keyof S extends infer TableName
129
- ? TableName extends keyof S
130
- ? "id" extends keyof S[TableName]
131
- ? S[TableName]["id"] extends TableId<any>
132
- ? never
133
- : SchemaValidationError<`Table "${TableName & string}" id column must be a branded ID type (created with id("${TableName & string}")).`>
134
- : never
135
- : never
136
- : never;
137
-
138
- export type ValidateNoSystemColumns<S extends EvoluSchema> =
139
- keyof S extends infer TableName
140
- ? TableName extends keyof S
141
- ? keyof S[TableName] extends infer ColumnName
142
- ? ColumnName extends keyof S[TableName]
143
- ? ColumnName extends
144
- | "createdAt"
145
- | "updatedAt"
146
- | "isDeleted"
147
- | "ownerId"
148
- ? SchemaValidationError<`Table "${TableName & string}" uses system column name "${ColumnName & string}". System columns (createdAt, updatedAt, isDeleted, ownerId) are added automatically.`>
149
- : never
150
- : never
151
- : never
152
- : never
153
- : never;
154
-
155
- export type ValidateColumnTypes<S extends EvoluSchema> =
156
- keyof S extends infer TableName
157
- ? TableName extends keyof S
158
- ? keyof S[TableName] extends infer ColumnName
159
- ? ColumnName extends keyof S[TableName]
160
- ? InferType<S[TableName][ColumnName]> extends SqliteValue
161
- ? never
162
- : SchemaValidationError<`Table "${TableName & string}" column "${ColumnName & string}" type is not compatible with SQLite. Column types must extend SqliteValue (string, number, Uint8Array, or null).`>
163
- : never
164
- : never
165
- : never
166
- : never;
167
-
168
- /** Schema validation error that shows clear, readable messages */
169
- export type SchemaValidationError<Message extends string> =
170
- `❌ Schema Error: ${Message}`;
171
-
172
118
  export type IndexesConfig = (
173
119
  create: (indexName: string) => Kysely.CreateIndexBuilder,
174
120
  ) => ReadonlyArray<Kysely.CreateIndexBuilder<any>>;
175
121
 
176
- export const evoluSchemaToDbSchema = (
177
- schema: EvoluSchema,
178
- indexesConfig?: IndexesConfig,
179
- ): DbSchema => {
180
- const tables = mapObject(
181
- schema,
182
- (table) => new Set(Object.keys(table).filter((k) => k !== "id")),
183
- );
184
-
185
- const indexes = indexesConfig
186
- ? indexesConfig(createIndex).map(
187
- (index): DbIndex => ({
188
- name: index.toOperationNode().name.name,
189
- sql: index.compile().sql,
190
- }),
191
- )
192
- : [];
193
-
194
- return { tables, indexes };
195
- };
196
-
197
122
  export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
198
123
  queryCallback: (
199
124
  db: Pick<
@@ -201,8 +126,8 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
201
126
  {
202
127
  [Table in keyof S]: {
203
128
  readonly [Column in keyof S[Table]]: Column extends "id"
204
- ? InferType<S[Table][Column]>
205
- : InferType<S[Table][Column]> | null;
129
+ ? StandardSchemaV1.InferOutput<S[Table][Column]>
130
+ : StandardSchemaV1.InferOutput<S[Table][Column]> | null;
206
131
  } & SystemColumns;
207
132
  } & {
208
133
  readonly evolu_history: {
@@ -225,53 +150,54 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
225
150
  >,
226
151
  ) => Kysely.SelectQueryBuilder<any, any, R>,
227
152
  options?: SqliteQueryOptions,
228
- ) => Query<Simplify<R>>;
153
+ ) => Query<S, Simplify<R>>;
229
154
 
230
155
  /**
231
156
  * System columns that are implicitly defined by Evolu.
232
157
  *
233
- * - `createdAt`: Set by Evolu on row creation, derived from {@link Timestamp}.
234
- * - `updatedAt`: Set by Evolu on every row change, derived from {@link Timestamp}.
158
+ * - `createdAt`: Set by Evolu on row creation, derived from Timestamp.
159
+ * - `updatedAt`: Set by Evolu on every row change, derived from Timestamp.
235
160
  * - `isDeleted`: Soft delete flag created by Evolu and used by the developer to
236
161
  * mark rows as deleted.
237
162
  * - `ownerId`: Represents ownership and logically partitions the database.
238
163
  */
239
- export const SystemColumns = object({
164
+ export const SystemColumns = /*#__PURE__*/ object({
240
165
  createdAt: DateIso,
241
166
  updatedAt: DateIso,
242
- isDeleted: nullOr(SqliteBoolean),
167
+ isDeleted: /*#__PURE__*/ nullOr(SqliteBoolean),
243
168
  ownerId: OwnerId,
244
169
  });
245
- export type SystemColumns = typeof SystemColumns.Type;
246
-
247
- export const systemColumns = readonly(
248
- new Set(Object.keys(SystemColumns.props)),
249
- );
250
-
251
- export const systemColumnsWithId = readonly([...systemColumns, "id"]);
170
+ export interface SystemColumns extends InferType<typeof SystemColumns> {}
252
171
 
253
172
  export type MutationKind = "insert" | "update" | "upsert";
254
173
 
174
+ /**
175
+ * Mutation function type. Accepts already-validated values — validation is the
176
+ * caller's responsibility using any Standard Schema library (Evolu Type, Zod,
177
+ * Valibot, ArkType, etc.).
178
+ *
179
+ * Evolu does not use SQL for mutations to ensure data can be deterministically
180
+ * merged without conflicts. Explicit mutations also allow Evolu to
181
+ * automatically update {@link SystemColumns} and encourage developers to
182
+ * consider the number of changes produced, unlike SQL where a single query can
183
+ * inadvertently generate a large volume of CRDT messages. Each mutation
184
+ * produces exactly one {@link CrdtMessage} containing all provided columns.
185
+ *
186
+ * Mutations never fail — values are already validated by the caller, and
187
+ * changes are stored locally in SQLite.
188
+ *
189
+ * - **insert**: all non-nullable columns required, nullable columns optional,
190
+ * `id` omitted (auto-generated)
191
+ * - **update**: only `id` required, everything else optional
192
+ * - **upsert**: like insert but `id` required too
193
+ */
255
194
  export type Mutation<S extends EvoluSchema, Kind extends MutationKind> = <
256
195
  TableName extends keyof S,
257
196
  >(
258
197
  table: TableName,
259
- props: InferInput<ObjectType<MutationMapping<S[TableName], Kind>>>,
198
+ values: MutationValues<S[TableName], Kind>,
260
199
  options?: MutationOptions,
261
- ) => Result<
262
- { readonly id: S[TableName]["id"]["Type"] },
263
- | ValidMutationSizeError
264
- | MergeObjectTypeErrors<ObjectType<MutationMapping<S[TableName], Kind>>>
265
- >;
266
-
267
- export type MutationMapping<
268
- P extends Record<string, AnyType>,
269
- M extends MutationKind,
270
- > = M extends "insert"
271
- ? InsertableProps<P>
272
- : M extends "update"
273
- ? UpdateableProps<P>
274
- : UpsertableProps<P>;
200
+ ) => { readonly id: StandardSchemaV1.InferOutput<S[TableName]["id"]> };
275
201
 
276
202
  export interface MutationOptions {
277
203
  /**
@@ -311,250 +237,216 @@ export interface MutationOptions {
311
237
  * { ownerId: sharedOwner.id },
312
238
  * );
313
239
  * ```
314
- *
315
- * @experimental
316
240
  */
317
241
  readonly ownerId?: OwnerId;
318
-
319
- /**
320
- * Only validate, don't mutate.
321
- *
322
- * For example, `onChange` handler can call `insert`/`update`/`upsert` with
323
- * `onlyValidate: true`.
324
- */
325
- readonly onlyValidate?: boolean;
326
242
  }
327
243
 
328
244
  export interface MutationChange extends DbChange {
329
- /** Owner of the change. If undefined, the change belongs to the AppOwner. */
330
- readonly ownerId?: OwnerId | undefined;
245
+ readonly ownerId: OwnerId;
331
246
  }
332
247
 
333
248
  /**
334
- * Type Factory to create insertable {@link Type}. It makes nullable Types
335
- * optional (so they are not required), omits Id, and ensures the
336
- * {@link maxMutationSize}.
337
- *
338
- * ### Example
339
- *
340
- * ```ts
341
- * const InsertableTodo = insertable(Schema.todo);
342
- * type InsertableTodo = typeof InsertableTodo.Type;
343
- * const todo = InsertableTodo.from({ title });
344
- * if (!todo.ok) return; // handle errors
345
- * ```
249
+ * Derives the expected values type for a mutation from a table's column schemas
250
+ * and a {@link MutationKind}.
346
251
  */
347
- export const insertable = <Props extends Record<string, AnyType>>(
348
- props: Props,
349
- ): ValidMutationSize<InsertableProps<Props>> => {
350
- const optionalNullable = nullableToOptional(props);
351
- const withoutId = omit(optionalNullable, "id");
352
- return validMutationSize(withoutId);
353
- };
252
+ export type MutationValues<
253
+ T extends TableSchema,
254
+ M extends MutationKind,
255
+ > = Simplify<
256
+ M extends "insert"
257
+ ? InsertValues<T>
258
+ : M extends "update"
259
+ ? UpdateValues<T>
260
+ : UpsertValues<T>
261
+ >;
354
262
 
355
- export type InsertableProps<Props extends Record<string, AnyType>> = Omit<
356
- NullableToOptionalProps<Props>,
263
+ /**
264
+ * Insert values: `id` omitted (auto-generated), nullable columns optional,
265
+ * non-nullable columns required.
266
+ */
267
+ export type InsertValues<T extends TableSchema> = Omit<
268
+ NullableColumnsToOptional<T>,
357
269
  "id"
358
270
  >;
359
271
 
360
- export type Insertable<Props extends Record<string, AnyType>> = InferInput<
361
- ObjectType<InsertableProps<Props>>
362
- >;
272
+ /**
273
+ * Update values: `id` required, all other columns optional. Includes
274
+ * `isDeleted` for soft deletes.
275
+ */
276
+ export type UpdateValues<T extends TableSchema> = {
277
+ readonly id: StandardSchemaV1.InferOutput<T["id"]>;
278
+ } & {
279
+ readonly [K in Exclude<keyof T, "id">]?: StandardSchemaV1.InferOutput<T[K]>;
280
+ } & {
281
+ readonly isDeleted?: SqliteBoolean;
282
+ };
363
283
 
364
284
  /**
365
- * Type Factory to create updateable {@link Type}. It makes everything except for
366
- * the `id` column optional (so they are not required) and ensures the
367
- * {@link maxMutationSize}.
368
- *
369
- * ### Example
370
- *
371
- * ```ts
372
- * const UpdateableTodo = updateable(Schema.todo);
373
- * type UpdateableTodo = typeof UpdateableTodo.Type;
374
- *
375
- * // `id` is required; all other fields are optional.
376
- * const todoResult = UpdateableTodo.from({
377
- * id: "123",
378
- * title: "New Title",
379
- * });
380
- * if (!todo.ok) return; // handle errors
381
- * ```
285
+ * Upsert values: `id` required, nullable columns optional, non-nullable columns
286
+ * required. Includes `isDeleted` for soft deletes.
382
287
  */
383
- export const updateable = <Props extends Record<string, AnyType>>(
384
- props: Props,
385
- ): ValidMutationSize<UpdateableProps<Props>> => {
386
- const propsWithIsDeleted = { ...props, isDeleted: SqliteBoolean };
387
- const updateableProps = mapObject(propsWithIsDeleted, (value, key) =>
388
- key === "id" ? value : optional(value),
389
- ) as UpdateableProps<Props>;
390
- return validMutationSize(object(updateableProps));
288
+ export type UpsertValues<T extends TableSchema> =
289
+ NullableColumnsToOptional<T> & {
290
+ readonly isDeleted?: SqliteBoolean;
291
+ };
292
+
293
+ export type ValidateSchemaHasId<S extends EvoluSchema> =
294
+ keyof S extends infer TableName
295
+ ? TableName extends keyof S
296
+ ? "id" extends keyof S[TableName]
297
+ ? never
298
+ : SchemaValidationError<`Table "${TableName & string}" is missing required id column.`>
299
+ : never
300
+ : never;
301
+
302
+ export type ValidateIdColumnType<S extends EvoluSchema> =
303
+ keyof S extends infer TableName
304
+ ? TableName extends keyof S
305
+ ? "id" extends keyof S[TableName]
306
+ ? StandardSchemaV1.InferOutput<S[TableName]["id"]> extends Id
307
+ ? never
308
+ : SchemaValidationError<`Table "${TableName & string}" id column output type must extend Id. Use id("${TableName & string}") from Evolu Type.`>
309
+ : never
310
+ : never
311
+ : never;
312
+
313
+ export type ValidateNoSystemColumns<S extends EvoluSchema> =
314
+ keyof S extends infer TableName
315
+ ? TableName extends keyof S
316
+ ? keyof S[TableName] extends infer ColumnName
317
+ ? ColumnName extends keyof S[TableName]
318
+ ? ColumnName extends
319
+ | "createdAt"
320
+ | "updatedAt"
321
+ | "isDeleted"
322
+ | "ownerId"
323
+ ? SchemaValidationError<`Table "${TableName & string}" uses system column name "${ColumnName & string}". System columns (createdAt, updatedAt, isDeleted, ownerId) are added automatically.`>
324
+ : never
325
+ : never
326
+ : never
327
+ : never
328
+ : never;
329
+
330
+ export type ValidateColumnTypes<S extends EvoluSchema> =
331
+ keyof S extends infer TableName
332
+ ? TableName extends keyof S
333
+ ? keyof S[TableName] extends infer ColumnName
334
+ ? ColumnName extends keyof S[TableName]
335
+ ? StandardSchemaV1.InferOutput<
336
+ S[TableName][ColumnName]
337
+ > extends SqliteValue
338
+ ? never
339
+ : SchemaValidationError<`Table "${TableName & string}" column "${ColumnName & string}" type is not compatible with SQLite. Column types must extend SqliteValue (string, number, Uint8Array, or null).`>
340
+ : never
341
+ : never
342
+ : never
343
+ : never;
344
+
345
+ /** Schema validation error that shows clear, readable messages */
346
+ export type SchemaValidationError<Message extends string> =
347
+ `❌ Schema Error: ${Message}`;
348
+
349
+ /** Makes columns whose output type includes `null` optional. */
350
+ export type NullableColumnsToOptional<T extends TableSchema> = {
351
+ readonly [K in RequiredColumnKeys<T>]: StandardSchemaV1.InferOutput<T[K]>;
352
+ } & {
353
+ readonly [K in OptionalColumnKeys<T>]?: StandardSchemaV1.InferOutput<T[K]>;
391
354
  };
392
355
 
393
- export type UpdateableProps<Props extends Record<string, AnyType>> = {
394
- [K in keyof Props]: K extends "id" ? Props[K] : OptionalType<Props[K]>;
395
- } & { isDeleted: OptionalType<typeof SqliteBoolean> };
356
+ export type RequiredColumnKeys<T extends TableSchema> = {
357
+ [K in keyof T]: null extends StandardSchemaV1.InferOutput<T[K]> ? never : K;
358
+ }[keyof T];
396
359
 
397
- export type Updateable<Props extends Record<string, AnyType>> = InferInput<
398
- ObjectType<UpdateableProps<Props>>
399
- >;
360
+ export type OptionalColumnKeys<T extends TableSchema> = {
361
+ [K in keyof T]: null extends StandardSchemaV1.InferOutput<T[K]> ? K : never;
362
+ }[keyof T];
363
+
364
+ export const systemColumns: ReadonlySet<string> = /*#__PURE__*/ new Set(
365
+ /*#__PURE__*/ Object.keys(SystemColumns.props),
366
+ );
367
+
368
+ export const systemColumnsWithId: ReadonlyArray<string> = [
369
+ ...systemColumns,
370
+ "id",
371
+ ];
372
+
373
+ export const evoluSchemaToSqliteSchema = <S extends EvoluSchema>(
374
+ schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
375
+ indexesConfig?: IndexesConfig,
376
+ ): SqliteSchema => {
377
+ const validSchema = schema as EvoluSchema;
378
+
379
+ const tables = mapObject(
380
+ validSchema,
381
+ (table) => new Set(Object.keys(table).filter((k) => k !== "id")),
382
+ );
383
+
384
+ const indexes = indexesConfig
385
+ ? indexesConfig(createIndex).map(
386
+ (index): SqliteIndex => ({
387
+ name: index.toOperationNode().name.name,
388
+ sql: index.compile().sql,
389
+ }),
390
+ )
391
+ : [];
392
+
393
+ return { tables, indexes };
394
+ };
400
395
 
401
396
  /**
402
- * Type Factory to create an upsertable Type. It makes nullable Types optional
403
- * (so they are not required) and ensures the {@link maxMutationSize}.
404
- *
405
- * Upsert is like insert, except it requires an ID. It's useful for inserting
406
- * rows with external ID via {@link createIdFromString}.
397
+ * Creates a query builder from a {@link EvoluSchema}.
407
398
  *
408
- * Note that it's not possible to upsert a row with `createdAt` nor `updatedAt`,
409
- * because they are derived from {@link CrdtMessage} timestamp. For external
410
- * createdAt, use a different column.
399
+ * Supports Kysely relation-style query composition (nested objects/arrays via
400
+ * JSON subqueries), such as {@link evoluJsonObjectFrom} and
401
+ * {@link evoluJsonArrayFrom}. These helpers are Evolu's safer SQLite variants of
402
+ * the
403
+ * {@link https://kysely.dev/docs/recipes/relations | Kysely relations recipe}.
411
404
  *
412
405
  * ### Example
413
406
  *
414
407
  * ```ts
415
- * const UpsertableTodo = upsertable(Schema.todo);
416
- * type UpsertableTodo = typeof UpsertableTodo.Type;
417
- * const todo = UpsertableTodo.from({
418
- * id,
419
- * title,
420
- * });
421
- * if (!todo.ok) return; // handle errors
408
+ * const Schema = {
409
+ * todo: {
410
+ * id: id("Todo"),
411
+ * title: NonEmptyString100,
412
+ * isCompleted: nullOr(SqliteBoolean),
413
+ * },
414
+ * };
415
+ *
416
+ * // Create a typed query builder (once per schema)
417
+ * const createQuery = createQueryBuilder(Schema);
418
+ *
419
+ * // Use it for all queries
420
+ * const todosQuery = createQuery((db) =>
421
+ * db.selectFrom("todo").select(["id", "title", "isCompleted"]),
422
+ * );
422
423
  * ```
423
424
  */
424
- export const upsertable = <Props extends Record<string, AnyType>>(
425
- props: Props,
426
- ): ValidMutationSize<UpsertableProps<Props>> => {
427
- const propsWithDefaults = {
428
- ...props,
429
- isDeleted: optional(SqliteBoolean),
430
- };
431
- return validMutationSize(nullableToOptional(propsWithDefaults));
432
- };
433
-
434
- export type UpsertableProps<Props extends Record<string, AnyType>> =
435
- NullableToOptionalProps<
436
- Props & {
437
- isDeleted: OptionalType<typeof SqliteBoolean>;
438
- }
439
- >;
440
-
441
- export type Upsertable<Props extends Record<string, AnyType>> = InferInput<
442
- ObjectType<UpsertableProps<Props>>
443
- >;
444
-
445
- export type InferEvoluSchemaError<S extends EvoluSchema> = {
446
- [Table in keyof S]: InferMutationTypeErrors<S[Table]>;
447
- }[keyof S];
448
-
449
- export type InferMutationTypeErrors<T extends Record<string, AnyType>> =
450
- | InferColumnErrors<T, "insert">
451
- | InferColumnErrors<T, "update">
452
- | InferColumnErrors<T, "upsert">;
453
-
454
- export type InferColumnErrors<
455
- T extends Record<string, AnyType>,
456
- M extends MutationKind,
457
- > = {
458
- [Column in keyof MutationMapping<T, M>]: InferErrors<
459
- MutationMapping<T, M>[Column]
460
- >;
461
- }[keyof MutationMapping<T, M>];
462
-
463
- export const DbIndex = object({ name: String, sql: String });
464
- export type DbIndex = typeof DbIndex.Type;
465
-
466
- export const DbSchema = object({
467
- tables: record(String, set(String)),
468
- indexes: array(DbIndex),
469
- });
470
- export type DbSchema = typeof DbSchema.Type;
471
-
472
- // TODO: Use a ref and update dbSchema on hot reloading to support
473
- // development workflows where schema changes without full app restart.
474
- export interface DbSchemaDep {
475
- readonly dbSchema: DbSchema;
476
- }
425
+ export const createQueryBuilder = <S extends EvoluSchema>(
426
+ _schema: S,
427
+ ): CreateQuery<S> => {
428
+ const createQuery: CreateQuery<S> = (queryCallback, options) => {
429
+ const compiledQuery = queryCallback(kysely as never).compile();
430
+ const sqliteQuery: SqliteQuery = {
431
+ sql: compiledQuery.sql as SafeSql,
432
+ parameters: compiledQuery.parameters as NonNullable<
433
+ SqliteQuery["parameters"]
434
+ >,
435
+ ...(options && { options }),
436
+ };
477
437
 
478
- /** Get the current database schema by reading SQLite metadata. */
479
- export const getDbSchema =
480
- (deps: SqliteDep) =>
481
- ({ allIndexes = false }: { allIndexes?: boolean } = {}): Result<
482
- DbSchema,
483
- SqliteError
484
- > => {
485
- const tables = createRecord<string, Set<string>>();
486
-
487
- const tableAndColumnInfoRows = deps.sqlite.exec(sql`
488
- select
489
- sqlite_master.name as tableName,
490
- table_info.name as columnName
491
- from
492
- sqlite_master
493
- join pragma_table_info(sqlite_master.name) as table_info;
494
- `);
495
-
496
- if (!tableAndColumnInfoRows.ok) return tableAndColumnInfoRows;
497
-
498
- tableAndColumnInfoRows.value.rows.forEach((row) => {
499
- const { tableName, columnName } = row as unknown as {
500
- tableName: string;
501
- columnName: string;
502
- };
503
- (tables[tableName] ??= new Set()).add(columnName);
504
- });
505
-
506
- const indexesRows = deps.sqlite.exec(
507
- allIndexes
508
- ? sql`
509
- select name, sql
510
- from sqlite_master
511
- where type = 'index' and name not like 'sqlite_%';
512
- `
513
- : sql`
514
- select name, sql
515
- from sqlite_master
516
- where
517
- type = 'index'
518
- and name not like 'sqlite_%'
519
- and name not like 'evolu_%';
520
- `,
521
- );
522
-
523
- if (!indexesRows.ok) return indexesRows;
524
-
525
- const indexes = indexesRows.value.rows.map(
526
- (row): DbIndex => ({
527
- name: row.name as string,
528
- /**
529
- * SQLite returns "CREATE INDEX" for "create index" for some reason.
530
- * Other keywords remain unchanged. We have to normalize the casing for
531
- * {@link indexesAreEqual} manually.
532
- */
533
- sql: (row.sql as string)
534
- .replace("CREATE INDEX", "create index")
535
- .replace("CREATE UNIQUE INDEX", "create unique index"),
536
- }),
537
- );
538
-
539
- return ok({ tables, indexes });
438
+ return sqliteQueryToSqliteQueryString(sqliteQuery) as never;
540
439
  };
541
440
 
542
- const indexesAreEqual = (self: DbIndex, that: DbIndex): boolean =>
543
- self.name === that.name && self.sql === that.sql;
441
+ return createQuery;
442
+ };
544
443
 
545
- export const ensureDbSchema =
444
+ export const ensureSqliteSchema =
546
445
  (deps: SqliteDep) =>
547
- (
548
- newSchema: DbSchema,
549
- currentSchema?: DbSchema,
550
- ): Result<void, SqliteError> => {
446
+ (newSchema: SqliteSchema, currentSchema?: SqliteSchema): void => {
551
447
  const queries: Array<SqliteQuery> = [];
552
448
 
553
- if (!currentSchema) {
554
- const dbSchema = getDbSchema(deps)();
555
- if (!dbSchema.ok) return dbSchema;
556
- currentSchema = dbSchema.value;
557
- }
449
+ currentSchema ??= getEvoluSqliteSchema(deps)();
558
450
 
559
451
  for (const [tableName, newColumns] of Object.entries(newSchema.tables)) {
560
452
  const currentColumns = getProperty(currentSchema.tables, tableName);
@@ -575,7 +467,7 @@ export const ensureDbSchema =
575
467
  .filter(
576
468
  (currentIndex) =>
577
469
  !newSchema.indexes.some((newIndex) =>
578
- indexesAreEqual(newIndex, currentIndex),
470
+ eqSqliteIndex(newIndex, currentIndex),
579
471
  ),
580
472
  )
581
473
  .forEach((index) => {
@@ -587,7 +479,7 @@ export const ensureDbSchema =
587
479
  .filter(
588
480
  (newIndex) =>
589
481
  !currentSchema.indexes.some((currentIndex) =>
590
- indexesAreEqual(newIndex, currentIndex),
482
+ eqSqliteIndex(newIndex, currentIndex),
591
483
  ),
592
484
  )
593
485
  .forEach((newIndex) => {
@@ -595,32 +487,15 @@ export const ensureDbSchema =
595
487
  });
596
488
 
597
489
  for (const query of queries) {
598
- const result = deps.sqlite.exec(query);
599
- if (!result.ok) return result;
490
+ deps.sqlite.exec(query);
600
491
  }
601
- return ok();
602
492
  };
603
493
 
604
- const createAppTable = (tableName: string, columns: ReadonlySet<string>) => sql`
605
- create table ${sql.identifier(tableName)} (
606
- "id" text,
607
- ${sql.raw(
608
- `${[...systemColumns, ...columns]
609
- // With strict tables and any type, data is preserved exactly as received
610
- // without any type affinity coercion. This allows storing any data type
611
- // while maintaining strict null enforcement for primary key columns.
612
- // TODO: Use proper SQLite types for system columns (text for createdAt,
613
- // updatedAt, ownerId, integer for isDeleted) instead of "any".
614
- .map((name) => `${sql.identifier(name).sql} any`)
615
- .join(", ")}, `,
616
- )}
617
- primary key ("ownerId", "id")
618
- )
619
- without rowid, strict;
620
- `;
494
+ export const getEvoluSqliteSchema = (deps: SqliteDep) => (): SqliteSchema =>
495
+ getSqliteSchema(deps)({ excludeIndexNamePrefix: "evolu_" });
621
496
 
622
497
  // https://kysely.dev/docs/recipes/splitting-query-building-and-execution
623
- export const kysely = new Kysely.Kysely({
498
+ export const kysely = /*#__PURE__*/ new Kysely.Kysely({
624
499
  dialect: {
625
500
  createAdapter: () => new Kysely.SqliteAdapter(),
626
501
  createDriver: () => new Kysely.DummyDriver(),
@@ -632,3 +507,18 @@ export const kysely = new Kysely.Kysely({
632
507
  });
633
508
 
634
509
  const createIndex = kysely.schema.createIndex.bind(kysely.schema);
510
+
511
+ const createAppTable = (tableName: string, columns: ReadonlySet<string>) => sql`
512
+ create table ${sql.identifier(tableName)} (
513
+ "id" text,
514
+ ${sql.raw(
515
+ [...systemColumns, ...columns]
516
+ // In STRICT tables, ANY columns accept any SQLite storage class without
517
+ // affinity coercion, while the primary key still enforces non-nullness.
518
+ .map((name) => `${sql.identifier(name).sql} any`)
519
+ .join(", "),
520
+ )},
521
+ primary key ("ownerId", "id")
522
+ )
523
+ without rowid, strict;
524
+ `;