@evolu/common 6.0.1-preview.8 → 7.0.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 (184) hide show
  1. package/README.md +2 -2
  2. package/dist/src/Array.d.ts +256 -12
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +215 -9
  5. package/dist/src/Assert.d.ts +0 -13
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +0 -15
  8. package/dist/src/Brand.d.ts +75 -0
  9. package/dist/src/Brand.d.ts.map +1 -0
  10. package/dist/src/Brand.js +1 -0
  11. package/dist/src/Buffer.d.ts +1 -1
  12. package/dist/src/Buffer.d.ts.map +1 -1
  13. package/dist/src/Buffer.js +8 -7
  14. package/dist/src/Cache.d.ts +44 -0
  15. package/dist/src/Cache.d.ts.map +1 -0
  16. package/dist/src/Cache.js +52 -0
  17. package/dist/src/Callbacks.d.ts +45 -12
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Callbacks.js +14 -7
  20. package/dist/src/Console.d.ts +31 -6
  21. package/dist/src/Console.d.ts.map +1 -1
  22. package/dist/src/Console.js +72 -9
  23. package/dist/src/Crypto.d.ts +56 -42
  24. package/dist/src/Crypto.d.ts.map +1 -1
  25. package/dist/src/Crypto.js +40 -53
  26. package/dist/src/Evolu/Db.d.ts +162 -74
  27. package/dist/src/Evolu/Db.d.ts.map +1 -1
  28. package/dist/src/Evolu/Db.js +284 -702
  29. package/dist/src/Evolu/Diff.d.ts +3 -3
  30. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  31. package/dist/src/Evolu/Diff.js +7 -5
  32. package/dist/src/Evolu/Evolu.d.ts +214 -134
  33. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  34. package/dist/src/Evolu/Evolu.js +189 -180
  35. package/dist/src/Evolu/Internal.d.ts +0 -2
  36. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  37. package/dist/src/Evolu/Internal.js +0 -2
  38. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  39. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  40. package/dist/src/Evolu/LocalAuth.js +174 -0
  41. package/dist/src/Evolu/Owner.d.ts +273 -120
  42. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  43. package/dist/src/Evolu/Owner.js +130 -104
  44. package/dist/src/Evolu/Platform.d.ts +9 -7
  45. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  46. package/dist/src/Evolu/Protocol.d.ts +268 -240
  47. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  48. package/dist/src/Evolu/Protocol.js +600 -454
  49. package/dist/src/Evolu/Public.d.ts +6 -8
  50. package/dist/src/Evolu/Public.d.ts.map +1 -1
  51. package/dist/src/Evolu/Public.js +2 -3
  52. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
  53. package/dist/src/Evolu/PublicKysely.js +3 -4
  54. package/dist/src/Evolu/Query.d.ts +2 -1
  55. package/dist/src/Evolu/Query.d.ts.map +1 -1
  56. package/dist/src/Evolu/Query.js +1 -1
  57. package/dist/src/Evolu/Relay.d.ts +91 -8
  58. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  59. package/dist/src/Evolu/Relay.js +214 -88
  60. package/dist/src/Evolu/Schema.d.ts +125 -47
  61. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  62. package/dist/src/Evolu/Schema.js +175 -31
  63. package/dist/src/Evolu/Storage.d.ts +249 -27
  64. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  65. package/dist/src/Evolu/Storage.js +198 -92
  66. package/dist/src/Evolu/Sync.d.ts +68 -13
  67. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  68. package/dist/src/Evolu/Sync.js +469 -20
  69. package/dist/src/Evolu/Timestamp.d.ts +83 -30
  70. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  71. package/dist/src/Evolu/Timestamp.js +79 -34
  72. package/dist/src/Identicon.d.ts +35 -0
  73. package/dist/src/Identicon.d.ts.map +1 -0
  74. package/dist/src/Identicon.js +143 -0
  75. package/dist/src/Instances.d.ts +34 -0
  76. package/dist/src/Instances.d.ts.map +1 -0
  77. package/dist/src/Instances.js +44 -0
  78. package/dist/src/Number.d.ts +4 -3
  79. package/dist/src/Number.d.ts.map +1 -1
  80. package/dist/src/Number.js +5 -4
  81. package/dist/src/Object.d.ts +10 -4
  82. package/dist/src/Object.d.ts.map +1 -1
  83. package/dist/src/Object.js +9 -3
  84. package/dist/src/Platform.d.ts +20 -0
  85. package/dist/src/Platform.d.ts.map +1 -0
  86. package/dist/src/Platform.js +22 -0
  87. package/dist/src/Random.d.ts +3 -2
  88. package/dist/src/Random.d.ts.map +1 -1
  89. package/dist/src/Relation.d.ts +79 -0
  90. package/dist/src/Relation.d.ts.map +1 -0
  91. package/dist/src/Relation.js +127 -0
  92. package/dist/src/Resources.d.ts +118 -0
  93. package/dist/src/Resources.d.ts.map +1 -0
  94. package/dist/src/Resources.js +197 -0
  95. package/dist/src/Result.d.ts +184 -52
  96. package/dist/src/Result.d.ts.map +1 -1
  97. package/dist/src/Result.js +30 -241
  98. package/dist/src/Skiplist.js +2 -1
  99. package/dist/src/Sqlite.d.ts +89 -5
  100. package/dist/src/Sqlite.d.ts.map +1 -1
  101. package/dist/src/Sqlite.js +136 -9
  102. package/dist/src/Task.d.ts +586 -0
  103. package/dist/src/Task.d.ts.map +1 -0
  104. package/dist/src/Task.js +469 -0
  105. package/dist/src/Time.d.ts +66 -1
  106. package/dist/src/Time.d.ts.map +1 -1
  107. package/dist/src/Time.js +99 -5
  108. package/dist/src/Type.d.ts +676 -343
  109. package/dist/src/Type.d.ts.map +1 -1
  110. package/dist/src/Type.js +718 -467
  111. package/dist/src/Types.d.ts +1 -75
  112. package/dist/src/Types.d.ts.map +1 -1
  113. package/dist/src/WebSocket.d.ts +5 -2
  114. package/dist/src/WebSocket.d.ts.map +1 -1
  115. package/dist/src/WebSocket.js +12 -18
  116. package/dist/src/Worker.d.ts +39 -11
  117. package/dist/src/Worker.d.ts.map +1 -1
  118. package/dist/src/Worker.js +22 -4
  119. package/dist/src/index.d.ts +8 -3
  120. package/dist/src/index.d.ts.map +1 -1
  121. package/dist/src/index.js +8 -3
  122. package/package.json +15 -14
  123. package/src/Array.ts +301 -19
  124. package/src/Assert.ts +0 -21
  125. package/src/Brand.ts +75 -0
  126. package/src/Buffer.ts +7 -7
  127. package/src/Cache.ts +85 -0
  128. package/src/Callbacks.ts +62 -22
  129. package/src/Console.ts +91 -11
  130. package/src/Crypto.ts +94 -90
  131. package/src/Evolu/Db.ts +519 -1026
  132. package/src/Evolu/Diff.ts +7 -5
  133. package/src/Evolu/Evolu.ts +477 -361
  134. package/src/Evolu/Internal.ts +0 -2
  135. package/src/Evolu/LocalAuth.ts +463 -0
  136. package/src/Evolu/Owner.ts +355 -228
  137. package/src/Evolu/Platform.ts +9 -9
  138. package/src/Evolu/Protocol.ts +842 -742
  139. package/src/Evolu/Public.ts +7 -14
  140. package/src/Evolu/PublicKysely.ts +4 -5
  141. package/src/Evolu/Query.ts +3 -2
  142. package/src/Evolu/Relay.ts +406 -103
  143. package/src/Evolu/Schema.ts +323 -91
  144. package/src/Evolu/Storage.ts +559 -137
  145. package/src/Evolu/Sync.ts +819 -36
  146. package/src/Evolu/Timestamp.ts +90 -58
  147. package/src/Identicon.ts +197 -0
  148. package/src/Instances.ts +90 -0
  149. package/src/Number.ts +6 -10
  150. package/src/Object.ts +13 -5
  151. package/src/Platform.ts +26 -0
  152. package/src/Random.ts +3 -2
  153. package/src/Relation.ts +234 -0
  154. package/src/Resources.ts +367 -0
  155. package/src/Result.ts +191 -54
  156. package/src/Skiplist.ts +1 -1
  157. package/src/Sqlite.ts +152 -17
  158. package/src/Task.ts +901 -0
  159. package/src/Time.ts +180 -5
  160. package/src/Type.ts +1135 -730
  161. package/src/Types.ts +1 -77
  162. package/src/WebSocket.ts +27 -25
  163. package/src/Worker.ts +72 -23
  164. package/src/index.ts +8 -3
  165. package/dist/src/Evolu/Config.d.ts +0 -69
  166. package/dist/src/Evolu/Config.d.ts.map +0 -1
  167. package/dist/src/Evolu/Config.js +0 -9
  168. package/dist/src/Evolu/Kysely.d.ts +0 -6
  169. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  170. package/dist/src/Evolu/Kysely.js +0 -21
  171. package/dist/src/ManyToManyMap.d.ts +0 -26
  172. package/dist/src/ManyToManyMap.d.ts.map +0 -1
  173. package/dist/src/ManyToManyMap.js +0 -92
  174. package/dist/src/NanoId.d.ts +0 -27
  175. package/dist/src/NanoId.d.ts.map +0 -1
  176. package/dist/src/NanoId.js +0 -6
  177. package/dist/src/Promise.d.ts +0 -180
  178. package/dist/src/Promise.d.ts.map +0 -1
  179. package/dist/src/Promise.js +0 -176
  180. package/src/Evolu/Config.ts +0 -83
  181. package/src/Evolu/Kysely.ts +0 -38
  182. package/src/ManyToManyMap.ts +0 -140
  183. package/src/NanoId.ts +0 -39
  184. package/src/Promise.ts +0 -295
@@ -1,19 +1,26 @@
1
- import { Kysely, SelectQueryBuilder } from "kysely";
2
- import { pack } from "msgpackr";
3
- import { assert } from "../Assert.js";
1
+ import * as Kysely from "kysely";
4
2
  import { mapObject, objectToEntries, ReadonlyRecord } from "../Object.js";
5
- import { err, ok, Result } from "../Result.js";
6
- import { SqliteBoolean, SqliteQueryOptions, SqliteValue } from "../Sqlite.js";
3
+ import { ok, Result } from "../Result.js";
4
+ import {
5
+ SafeSql,
6
+ sql,
7
+ SqliteBoolean,
8
+ SqliteDep,
9
+ SqliteError,
10
+ SqliteQuery,
11
+ SqliteQueryOptions,
12
+ SqliteValue,
13
+ } from "../Sqlite.js";
7
14
  import {
8
15
  AnyType,
9
- brand,
10
- BrandType,
11
- createTypeErrorFormatter,
12
- DateIsoString,
13
- IdType,
16
+ array,
17
+ createIdFromString,
18
+ DateIso,
19
+ IdBytes,
14
20
  InferErrors,
15
21
  InferInput,
16
22
  InferType,
23
+ maxMutationSize,
17
24
  MergeObjectTypeErrors,
18
25
  nullableToOptional,
19
26
  NullableToOptionalProps,
@@ -23,16 +30,18 @@ import {
23
30
  omit,
24
31
  optional,
25
32
  OptionalType,
33
+ String,
34
+ TableId,
26
35
  Type,
27
- TypeError,
36
+ ValidMutationSize,
37
+ validMutationSize,
38
+ ValidMutationSizeError,
28
39
  } from "../Type.js";
29
40
  import { Simplify } from "../Types.js";
30
- import { DbSchema } from "./Db.js";
31
- import { createIndexes, DbIndexesBuilder } from "./Kysely.js";
32
- import { AppOwner, ShardOwner, SharedOwner } from "./Owner.js";
33
- import { BinaryId, maxProtocolMessageRangesSize } from "./Protocol.js";
41
+ import { AppOwner, OwnerId } from "./Owner.js";
34
42
  import { Query, Row } from "./Query.js";
35
- import { BinaryTimestamp } from "./Timestamp.js";
43
+ import { CrdtMessage, DbChange } from "./Storage.js";
44
+ import { Timestamp, TimestampBytes } from "./Timestamp.js";
36
45
 
37
46
  /**
38
47
  * Defines the schema of an Evolu database.
@@ -71,7 +80,7 @@ import { BinaryTimestamp } from "./Timestamp.js";
71
80
  export type EvoluSchema = ReadonlyRecord<
72
81
  string,
73
82
  // TypeScript errors are cryptic so we use ValidateSchema.
74
- ReadonlyRecord<string, Type<any, any, any, any, any>>
83
+ ReadonlyRecord<string, Type<any, any, any, any, any, any>>
75
84
  >;
76
85
 
77
86
  /**
@@ -84,17 +93,17 @@ export type EvoluSchema = ReadonlyRecord<
84
93
  *
85
94
  * 1. All tables must have an 'id' column
86
95
  * 2. The 'id' column must be a branded ID type (created with id() function)
87
- * 3. Tables cannot use default column names (createdAt, updatedAt, isDeleted)
96
+ * 3. Tables cannot use system column names (createdAt, updatedAt, isDeleted)
88
97
  * 4. All column types must be compatible with SQLite (extend SqliteValue)
89
98
  */
90
99
  export type ValidateSchema<S extends EvoluSchema> =
91
100
  ValidateSchemaHasId<S> extends never
92
101
  ? ValidateIdColumnType<S> extends never
93
- ? ValidateNoDefaultColumns<S> extends never
102
+ ? ValidateNoSystemColumns<S> extends never
94
103
  ? ValidateColumnTypes<S> extends never
95
104
  ? S
96
105
  : ValidateColumnTypes<S>
97
- : ValidateNoDefaultColumns<S>
106
+ : ValidateNoSystemColumns<S>
98
107
  : ValidateIdColumnType<S>
99
108
  : ValidateSchemaHasId<S>;
100
109
 
@@ -111,20 +120,24 @@ export type ValidateIdColumnType<S extends EvoluSchema> =
111
120
  keyof S extends infer TableName
112
121
  ? TableName extends keyof S
113
122
  ? "id" extends keyof S[TableName]
114
- ? S[TableName]["id"] extends IdType<any>
123
+ ? S[TableName]["id"] extends TableId<any>
115
124
  ? never
116
125
  : SchemaValidationError<`Table "${TableName & string}" id column must be a branded ID type (created with id("${TableName & string}")).`>
117
126
  : never
118
127
  : never
119
128
  : never;
120
129
 
121
- export type ValidateNoDefaultColumns<S extends EvoluSchema> =
130
+ export type ValidateNoSystemColumns<S extends EvoluSchema> =
122
131
  keyof S extends infer TableName
123
132
  ? TableName extends keyof S
124
133
  ? keyof S[TableName] extends infer ColumnName
125
134
  ? ColumnName extends keyof S[TableName]
126
- ? ColumnName extends "createdAt" | "updatedAt" | "isDeleted"
127
- ? SchemaValidationError<`Table "${TableName & string}" uses default column name "${ColumnName & string}". Default columns (createdAt, updatedAt, isDeleted) are added automatically.`>
135
+ ? ColumnName extends
136
+ | "createdAt"
137
+ | "updatedAt"
138
+ | "isDeleted"
139
+ | "ownerId"
140
+ ? SchemaValidationError<`Table "${TableName & string}" uses system column name "${ColumnName & string}". System columns (createdAt, updatedAt, isDeleted, ownerId) are added automatically.`>
128
141
  : never
129
142
  : never
130
143
  : never
@@ -148,9 +161,13 @@ export type ValidateColumnTypes<S extends EvoluSchema> =
148
161
  export type SchemaValidationError<Message extends string> =
149
162
  `❌ Schema Error: ${Message}`;
150
163
 
164
+ export type IndexesConfig = (
165
+ create: (indexName: string) => Kysely.CreateIndexBuilder,
166
+ ) => ReadonlyArray<Kysely.CreateIndexBuilder<any>>;
167
+
151
168
  export const evoluSchemaToDbSchema = (
152
169
  schema: EvoluSchema,
153
- indexes?: DbIndexesBuilder,
170
+ indexesConfig?: IndexesConfig,
154
171
  ): DbSchema => {
155
172
  const tables = objectToEntries(schema).map(([tableName, table]) => ({
156
173
  name: tableName,
@@ -159,34 +176,33 @@ export const evoluSchemaToDbSchema = (
159
176
  .map(([k]) => k),
160
177
  }));
161
178
 
162
- const dbSchema = { tables, indexes: createIndexes(indexes) };
163
-
164
- assert(
165
- DbSchema.is(dbSchema),
166
- "Invalid EvoluSchema: Table and column names must use only characters A-Za-z0-9_- and be at most 256 characters long.",
167
- );
179
+ const indexes = indexesConfig
180
+ ? indexesConfig(createIndex).map(
181
+ (index): DbIndex => ({
182
+ name: index.toOperationNode().name.name,
183
+ sql: index.compile().sql,
184
+ }),
185
+ )
186
+ : [];
168
187
 
169
- return dbSchema;
188
+ return { tables, indexes };
170
189
  };
171
190
 
172
191
  export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
173
192
  queryCallback: (
174
193
  db: Pick<
175
- Kysely<
194
+ Kysely.Kysely<
176
195
  {
177
196
  [Table in keyof S]: {
178
- readonly [Column in keyof S[Table]]: Column extends
179
- | "id"
180
- | "createdAt"
181
- | "updatedAt"
197
+ readonly [Column in keyof S[Table]]: Column extends "id"
182
198
  ? InferType<S[Table][Column]>
183
199
  : InferType<S[Table][Column]> | null;
184
- } & DefaultColumns;
200
+ } & SystemColumns;
185
201
  } & {
186
202
  readonly evolu_history: {
187
- readonly timestamp: BinaryTimestamp;
203
+ readonly timestamp: TimestampBytes;
188
204
  readonly table: keyof S;
189
- readonly id: BinaryId;
205
+ readonly id: IdBytes;
190
206
  readonly column: string;
191
207
  readonly value: SqliteValue;
192
208
  };
@@ -194,16 +210,28 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
194
210
  >,
195
211
  "selectFrom" | "fn" | "with" | "withRecursive"
196
212
  >,
197
- ) => SelectQueryBuilder<any, any, R>,
213
+ ) => Kysely.SelectQueryBuilder<any, any, R>,
198
214
  options?: SqliteQueryOptions,
199
215
  ) => Query<Simplify<R>>;
200
216
 
201
- export const DefaultColumns = object({
202
- createdAt: DateIsoString,
203
- updatedAt: DateIsoString,
217
+ /**
218
+ * System columns that are implicitly defined by Evolu.
219
+ *
220
+ * - `createdAt`: Set by Evolu on row creation, derived from {@link Timestamp}.
221
+ * - `updatedAt`: Set by Evolu on every row change, derived from {@link Timestamp}.
222
+ * - `isDeleted`: Soft delete flag created by Evolu and used by the developer to
223
+ * mark rows as deleted.
224
+ * - `ownerId`: Represents ownership and logically partitions the database.
225
+ */
226
+ export const SystemColumns = object({
227
+ createdAt: DateIso,
228
+ updatedAt: DateIso,
204
229
  isDeleted: nullOr(SqliteBoolean),
230
+ ownerId: OwnerId,
205
231
  });
206
- export type DefaultColumns = typeof DefaultColumns.Type;
232
+ export type SystemColumns = typeof SystemColumns.Type;
233
+
234
+ export const systemColumns = Object.keys(SystemColumns.props);
207
235
 
208
236
  export type MutationKind = "insert" | "update" | "upsert";
209
237
 
@@ -229,7 +257,48 @@ export type MutationMapping<
229
257
  : UpsertableProps<P>;
230
258
 
231
259
  export interface MutationOptions {
260
+ /**
261
+ * Called after the mutation is completed and the local state is updated.
262
+ * Useful for triggering side effects (e.g., notifications, UI updates) after
263
+ * insert, update, or upsert.
264
+ */
232
265
  readonly onComplete?: () => void;
266
+
267
+ /**
268
+ * Specifies the owner ID for this mutation. If omitted, the default
269
+ * {@link AppOwner} is used.
270
+ *
271
+ * The owner must be used with `evolu.useOwner()` to enable sync. Mutations
272
+ * with unused owners are stored locally but not synced until the owner is
273
+ * used.
274
+ *
275
+ * ### Example
276
+ *
277
+ * ```ts
278
+ * // Partition your own data by project (derived from your AppOwner)
279
+ * const projectOwner = deriveShardOwner(appOwner, [
280
+ * "project",
281
+ * projectId,
282
+ * ]);
283
+ * evolu.insert(
284
+ * "task",
285
+ * { title: "Task 1" },
286
+ * { ownerId: projectOwner.id },
287
+ * );
288
+ *
289
+ * // Collaborative data (independent owner shared with others)
290
+ * const sharedOwner = createSharedOwner(sharedSecret);
291
+ * evolu.insert(
292
+ * "comment",
293
+ * { text: "Hello" },
294
+ * { ownerId: sharedOwner.id },
295
+ * );
296
+ * ```
297
+ *
298
+ * @experimental
299
+ */
300
+ readonly ownerId?: OwnerId;
301
+
233
302
  /**
234
303
  * Only validate, don't mutate.
235
304
  *
@@ -237,50 +306,17 @@ export interface MutationOptions {
237
306
  * `onlyValidate: true`.
238
307
  */
239
308
  readonly onlyValidate?: boolean;
240
-
241
- /**
242
- * The owner to use for this mutation. Can be a {@link ShardOwner} for sharding
243
- * app data or a {@link SharedOwner} for collaborative write access. If
244
- * omitted, defaults to the app's {@link AppOwner}.
245
- */
246
- readonly owner?: ShardOwner | SharedOwner;
247
309
  }
248
310
 
249
- /**
250
- * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
251
- * the {@link maxProtocolMessageRangesSize}. The max size is 640KB in bytes,
252
- * measured via MessagePack. Evolu Protocol DbChange will be smaller thanks to
253
- * various optimizations.
254
- */
255
- export const maxMutationSize = 655360;
256
-
257
- const validMutationSize = <T extends AnyType>(type: T) =>
258
- brand("ValidMutationSize", type, (value) =>
259
- pack(value).byteLength <= maxMutationSize
260
- ? ok(value)
261
- : err<ValidMutationSizeError>({ type: "ValidMutationSize", value }),
262
- );
263
-
264
- export interface ValidMutationSizeError
265
- extends TypeError<"ValidMutationSize"> {}
266
-
267
- export const formatValidMutationSizeError =
268
- createTypeErrorFormatter<ValidMutationSizeError>(
269
- (error) =>
270
- `The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`,
271
- );
272
-
273
- export type ValidMutationSize<Props extends Record<string, AnyType>> =
274
- BrandType<
275
- ObjectType<Props>,
276
- "ValidMutationSize",
277
- ValidMutationSizeError,
278
- InferErrors<ObjectType<Props>>
279
- >;
311
+ export interface MutationChange extends DbChange {
312
+ /** Owner of the change. If undefined, the change belongs to the AppOwner. */
313
+ readonly ownerId?: OwnerId | undefined;
314
+ }
280
315
 
281
316
  /**
282
317
  * Type Factory to create insertable {@link Type}. It makes nullable Types
283
- * optional, omits Id, and ensures the {@link maxMutationSize}.
318
+ * optional (so they are not required), omits Id, and ensures the
319
+ * {@link maxMutationSize}.
284
320
  *
285
321
  * ### Example
286
322
  *
@@ -310,7 +346,7 @@ export type Insertable<Props extends Record<string, AnyType>> = InferInput<
310
346
 
311
347
  /**
312
348
  * Type Factory to create updateable {@link Type}. It makes everything except for
313
- * the `id` column partial (i.e. optional) and ensures the
349
+ * the `id` column optional (so they are not required) and ensures the
314
350
  * {@link maxMutationSize}.
315
351
  *
316
352
  * ### Example
@@ -346,25 +382,44 @@ export type Updateable<Props extends Record<string, AnyType>> = InferInput<
346
382
  >;
347
383
 
348
384
  /**
349
- * Type Factory to create upsertable Type. It makes nullable Types optional and
350
- * ensures the {@link maxMutationSize}.
385
+ * Type Factory to create an upsertable Type. It makes nullable Types optional
386
+ * (so they are not required) and ensures the {@link maxMutationSize}.
387
+ *
388
+ * Upsert is like insert, except it requires an ID. It's useful for inserting
389
+ * rows with external ID via {@link createIdFromString}.
390
+ *
391
+ * Note that it's not possible to upsert a row with `createdAt` nor `updatedAt`,
392
+ * because they are derived from {@link CrdtMessage} timestamp. For external
393
+ * createdAt, use a different column.
351
394
  *
352
395
  * ### Example
353
396
  *
354
397
  * ```ts
355
398
  * const UpsertableTodo = upsertable(Schema.todo);
356
399
  * type UpsertableTodo = typeof UpsertableTodo.Type;
357
- * const todo = UpsertableTodo.from({ id, title });
400
+ * const todo = UpsertableTodo.from({
401
+ * id,
402
+ * title,
403
+ * });
358
404
  * if (!todo.ok) return; // handle errors
359
405
  * ```
360
406
  */
361
407
  export const upsertable = <Props extends Record<string, AnyType>>(
362
408
  props: Props,
363
- ): ValidMutationSize<UpsertableProps<Props>> =>
364
- validMutationSize(nullableToOptional(props));
409
+ ): ValidMutationSize<UpsertableProps<Props>> => {
410
+ const propsWithDefaults = {
411
+ ...props,
412
+ isDeleted: optional(SqliteBoolean),
413
+ };
414
+ return validMutationSize(nullableToOptional(propsWithDefaults));
415
+ };
365
416
 
366
417
  export type UpsertableProps<Props extends Record<string, AnyType>> =
367
- NullableToOptionalProps<Props>;
418
+ NullableToOptionalProps<
419
+ Props & {
420
+ isDeleted: OptionalType<typeof SqliteBoolean>;
421
+ }
422
+ >;
368
423
 
369
424
  export type Upsertable<Props extends Record<string, AnyType>> = InferInput<
370
425
  ObjectType<UpsertableProps<Props>>
@@ -387,3 +442,180 @@ export type InferColumnErrors<
387
442
  MutationMapping<T, M>[Column]
388
443
  >;
389
444
  }[keyof MutationMapping<T, M>];
445
+
446
+ export const DbTable = object({
447
+ name: String,
448
+ columns: array(String),
449
+ });
450
+ export type DbTable = typeof DbTable.Type;
451
+
452
+ export const DbIndex = object({ name: String, sql: String });
453
+ export type DbIndex = typeof DbIndex.Type;
454
+
455
+ export const DbSchema = object({
456
+ tables: array(DbTable),
457
+ indexes: array(DbIndex),
458
+ });
459
+ export type DbSchema = typeof DbSchema.Type;
460
+
461
+ /** Get the current database schema by reading SQLite metadata. */
462
+ export const getDbSchema =
463
+ (deps: SqliteDep) =>
464
+ ({ allIndexes = false }: { allIndexes?: boolean } = {}): Result<
465
+ DbSchema,
466
+ SqliteError
467
+ > => {
468
+ const map = new Map<string, Array<string>>();
469
+
470
+ const tableAndColumnInfoRows = deps.sqlite.exec(sql`
471
+ select
472
+ sqlite_master.name as tableName,
473
+ table_info.name as columnName
474
+ from
475
+ sqlite_master
476
+ join pragma_table_info(sqlite_master.name) as table_info;
477
+ `);
478
+
479
+ if (!tableAndColumnInfoRows.ok) return tableAndColumnInfoRows;
480
+
481
+ tableAndColumnInfoRows.value.rows.forEach((row) => {
482
+ const { tableName, columnName } = row as unknown as {
483
+ tableName: string;
484
+ columnName: string;
485
+ };
486
+ if (!map.has(tableName)) map.set(tableName, []);
487
+ map.get(tableName)?.push(columnName);
488
+ });
489
+
490
+ const tables = Array.from(map, ([name, columns]) => ({ name, columns }));
491
+
492
+ const indexesRows = deps.sqlite.exec(
493
+ allIndexes
494
+ ? sql`
495
+ select name, sql
496
+ from sqlite_master
497
+ where type = 'index' and name not like 'sqlite_%';
498
+ `
499
+ : sql`
500
+ select name, sql
501
+ from sqlite_master
502
+ where
503
+ type = 'index'
504
+ and name not like 'sqlite_%'
505
+ and name not like 'evolu_%';
506
+ `,
507
+ );
508
+
509
+ if (!indexesRows.ok) return indexesRows;
510
+
511
+ const indexes = indexesRows.value.rows.map(
512
+ (row): DbIndex => ({
513
+ name: row.name as string,
514
+ /**
515
+ * SQLite returns "CREATE INDEX" for "create index" for some reason.
516
+ * Other keywords remain unchanged. We have to normalize the casing for
517
+ * {@link indexesAreEqual} manually.
518
+ */
519
+ sql: (row.sql as string)
520
+ .replace("CREATE INDEX", "create index")
521
+ .replace("CREATE UNIQUE INDEX", "create unique index"),
522
+ }),
523
+ );
524
+
525
+ return ok({ tables, indexes });
526
+ };
527
+
528
+ const indexesAreEqual = (self: DbIndex, that: DbIndex): boolean =>
529
+ self.name === that.name && self.sql === that.sql;
530
+
531
+ export const ensureDbSchema =
532
+ (deps: SqliteDep) =>
533
+ (
534
+ newSchema: DbSchema,
535
+ currentSchema: DbSchema,
536
+ options?: { ignoreIndexes: boolean },
537
+ ): Result<void, SqliteError> => {
538
+ const queries: Array<SqliteQuery> = [];
539
+
540
+ newSchema.tables.forEach((newTable) => {
541
+ const currentTable = currentSchema.tables.find(
542
+ (t) => t.name === newTable.name,
543
+ );
544
+ if (!currentTable) {
545
+ queries.push(createAppTable(newTable));
546
+ } else {
547
+ newTable.columns
548
+ .filter((newColumn) => !currentTable.columns.includes(newColumn))
549
+ .forEach((newColumn) => {
550
+ queries.push(sql`
551
+ alter table ${sql.identifier(newTable.name)}
552
+ add column ${sql.identifier(newColumn)} blob;
553
+ `);
554
+ });
555
+ }
556
+ });
557
+
558
+ if (options?.ignoreIndexes !== true) {
559
+ // Remove current indexes that are not in the newSchema.
560
+ currentSchema.indexes
561
+ .filter(
562
+ (currentIndex) =>
563
+ !newSchema.indexes.some((newIndex) =>
564
+ indexesAreEqual(newIndex, currentIndex),
565
+ ),
566
+ )
567
+ .forEach((index) => {
568
+ queries.push(sql`drop index ${sql.identifier(index.name)};`);
569
+ });
570
+
571
+ // Add new indexes that are not in the currentSchema.
572
+ newSchema.indexes
573
+ .filter(
574
+ (newIndex) =>
575
+ !currentSchema.indexes.some((currentIndex) =>
576
+ indexesAreEqual(newIndex, currentIndex),
577
+ ),
578
+ )
579
+ .forEach((newIndex) => {
580
+ queries.push({ sql: `${newIndex.sql};` as SafeSql, parameters: [] });
581
+ });
582
+ }
583
+
584
+ for (const query of queries) {
585
+ const result = deps.sqlite.exec(query);
586
+ if (!result.ok) return result;
587
+ }
588
+ return ok();
589
+ };
590
+
591
+ const createAppTable = (table: DbTable) => sql`
592
+ create table ${sql.identifier(table.name)} (
593
+ "id" text,
594
+ ${sql.raw(
595
+ `${systemColumns
596
+ .concat(table.columns)
597
+ .filter((c) => c !== "id")
598
+ // With strict tables and any type, data is preserved exactly as received
599
+ // without any type affinity coercion. This allows storing any data type
600
+ // while maintaining strict null enforcement for primary key columns.
601
+ .map((name) => `${sql.identifier(name).sql} any`)
602
+ .join(", ")}`,
603
+ )},
604
+ primary key ("ownerId", "id")
605
+ )
606
+ without rowid, strict;
607
+ `;
608
+
609
+ // https://kysely.dev/docs/recipes/splitting-query-building-and-execution
610
+ export const kysely = new Kysely.Kysely({
611
+ dialect: {
612
+ createAdapter: () => new Kysely.SqliteAdapter(),
613
+ createDriver: () => new Kysely.DummyDriver(),
614
+ createIntrospector() {
615
+ throw new Error("Not implemeneted");
616
+ },
617
+ createQueryCompiler: () => new Kysely.SqliteQueryCompiler(),
618
+ },
619
+ });
620
+
621
+ const createIndex = kysely.schema.createIndex.bind(kysely.schema);