@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20

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 (140) hide show
  1. package/dist/src/Assert.d.ts.map +1 -1
  2. package/dist/src/Assert.js +1 -1
  3. package/dist/src/Buffer.d.ts +1 -1
  4. package/dist/src/Buffer.d.ts.map +1 -1
  5. package/dist/src/Buffer.js +1 -1
  6. package/dist/src/CallbackRegistry.d.ts +53 -0
  7. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  8. package/dist/src/CallbackRegistry.js +25 -0
  9. package/dist/src/Console.d.ts +31 -6
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +72 -9
  12. package/dist/src/Crypto.d.ts +48 -37
  13. package/dist/src/Crypto.d.ts.map +1 -1
  14. package/dist/src/Crypto.js +27 -50
  15. package/dist/src/Evolu/Db.d.ts +138 -66
  16. package/dist/src/Evolu/Db.d.ts.map +1 -1
  17. package/dist/src/Evolu/Db.js +248 -645
  18. package/dist/src/Evolu/Diff.d.ts +3 -3
  19. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  20. package/dist/src/Evolu/Diff.js +7 -5
  21. package/dist/src/Evolu/Evolu.d.ts +79 -116
  22. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  23. package/dist/src/Evolu/Evolu.js +275 -132
  24. package/dist/src/Evolu/Internal.d.ts +0 -2
  25. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  26. package/dist/src/Evolu/Internal.js +0 -2
  27. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  28. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  29. package/dist/src/Evolu/LocalAuth.js +171 -0
  30. package/dist/src/Evolu/Owner.d.ts +129 -83
  31. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  32. package/dist/src/Evolu/Owner.js +80 -89
  33. package/dist/src/Evolu/Platform.d.ts +9 -7
  34. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  35. package/dist/src/Evolu/Protocol.d.ts +114 -191
  36. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  37. package/dist/src/Evolu/Protocol.js +409 -416
  38. package/dist/src/Evolu/Public.d.ts +6 -8
  39. package/dist/src/Evolu/Public.d.ts.map +1 -1
  40. package/dist/src/Evolu/Public.js +2 -3
  41. package/dist/src/Evolu/PublicKysely.js +3 -3
  42. package/dist/src/Evolu/Relay.d.ts +1 -2
  43. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  44. package/dist/src/Evolu/Relay.js +11 -9
  45. package/dist/src/Evolu/Schema.d.ts +88 -27
  46. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  47. package/dist/src/Evolu/Schema.js +141 -24
  48. package/dist/src/Evolu/Storage.d.ts +158 -14
  49. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  50. package/dist/src/Evolu/Storage.js +32 -32
  51. package/dist/src/Evolu/Sync.d.ts +77 -13
  52. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  53. package/dist/src/Evolu/Sync.js +453 -20
  54. package/dist/src/Evolu/Timestamp.d.ts +29 -27
  55. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  56. package/dist/src/Evolu/Timestamp.js +20 -18
  57. package/dist/src/ManyToManyMap.d.ts +74 -10
  58. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  59. package/dist/src/ManyToManyMap.js +41 -6
  60. package/dist/src/Random.d.ts +3 -2
  61. package/dist/src/Random.d.ts.map +1 -1
  62. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  63. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  64. package/dist/src/RefCountedResourceManager.js +197 -0
  65. package/dist/src/Result.d.ts +144 -22
  66. package/dist/src/Result.d.ts.map +1 -1
  67. package/dist/src/Result.js +5 -2
  68. package/dist/src/Sqlite.d.ts +20 -4
  69. package/dist/src/Sqlite.d.ts.map +1 -1
  70. package/dist/src/Sqlite.js +50 -8
  71. package/dist/src/Task.d.ts +511 -0
  72. package/dist/src/Task.d.ts.map +1 -0
  73. package/dist/src/Task.js +410 -0
  74. package/dist/src/Time.d.ts +59 -0
  75. package/dist/src/Time.d.ts.map +1 -1
  76. package/dist/src/Time.js +87 -4
  77. package/dist/src/Type.d.ts +431 -341
  78. package/dist/src/Type.d.ts.map +1 -1
  79. package/dist/src/Type.js +458 -466
  80. package/dist/src/WebSocket.d.ts +5 -2
  81. package/dist/src/WebSocket.d.ts.map +1 -1
  82. package/dist/src/WebSocket.js +12 -13
  83. package/dist/src/Worker.d.ts +39 -11
  84. package/dist/src/Worker.d.ts.map +1 -1
  85. package/dist/src/Worker.js +22 -4
  86. package/dist/src/index.d.ts +2 -3
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/index.js +2 -3
  89. package/package.json +7 -7
  90. package/src/Assert.ts +2 -4
  91. package/src/Buffer.ts +1 -1
  92. package/src/CallbackRegistry.ts +84 -0
  93. package/src/Console.ts +91 -11
  94. package/src/Crypto.ts +78 -91
  95. package/src/Evolu/Db.ts +455 -947
  96. package/src/Evolu/Diff.ts +7 -5
  97. package/src/Evolu/Evolu.ts +545 -307
  98. package/src/Evolu/Internal.ts +0 -2
  99. package/src/Evolu/LocalAuth.ts +422 -0
  100. package/src/Evolu/Owner.ts +191 -131
  101. package/src/Evolu/Platform.ts +9 -9
  102. package/src/Evolu/Protocol.ts +536 -653
  103. package/src/Evolu/Public.ts +7 -9
  104. package/src/Evolu/PublicKysely.ts +3 -3
  105. package/src/Evolu/Relay.ts +17 -12
  106. package/src/Evolu/Schema.ts +271 -66
  107. package/src/Evolu/Storage.ts +263 -55
  108. package/src/Evolu/Sync.ts +758 -37
  109. package/src/Evolu/Timestamp.ts +30 -35
  110. package/src/ManyToManyMap.ts +127 -24
  111. package/src/Random.ts +3 -2
  112. package/src/RefCountedResourceManager.ts +368 -0
  113. package/src/Result.ts +149 -23
  114. package/src/Sqlite.ts +59 -24
  115. package/src/Task.ts +779 -0
  116. package/src/Time.ts +168 -4
  117. package/src/Type.ts +657 -695
  118. package/src/WebSocket.ts +23 -17
  119. package/src/Worker.ts +72 -23
  120. package/src/index.ts +2 -3
  121. package/dist/src/Callbacks.d.ts +0 -20
  122. package/dist/src/Callbacks.d.ts.map +0 -1
  123. package/dist/src/Callbacks.js +0 -18
  124. package/dist/src/Evolu/Config.d.ts +0 -82
  125. package/dist/src/Evolu/Config.d.ts.map +0 -1
  126. package/dist/src/Evolu/Config.js +0 -9
  127. package/dist/src/Evolu/Kysely.d.ts +0 -6
  128. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  129. package/dist/src/Evolu/Kysely.js +0 -21
  130. package/dist/src/NanoId.d.ts +0 -27
  131. package/dist/src/NanoId.d.ts.map +0 -1
  132. package/dist/src/NanoId.js +0 -6
  133. package/dist/src/Promise.d.ts +0 -180
  134. package/dist/src/Promise.d.ts.map +0 -1
  135. package/dist/src/Promise.js +0 -176
  136. package/src/Callbacks.ts +0 -43
  137. package/src/Evolu/Config.ts +0 -97
  138. package/src/Evolu/Kysely.ts +0 -38
  139. package/src/NanoId.ts +0 -39
  140. package/src/Promise.ts +0 -295
@@ -5,18 +5,17 @@
5
5
  */
6
6
 
7
7
  export { createEvolu } from "./Evolu.js";
8
- export type { Evolu, EvoluDeps, EvoluError } from "./Evolu.js";
8
+ export type { Evolu, EvoluConfig, EvoluDeps, EvoluError } from "./Evolu.js";
9
9
  export * from "./Owner.js";
10
- export { binaryIdToId, idToBinaryId } from "./Protocol.js";
11
- export type { BinaryId } from "./Protocol.js";
10
+ export * from "./LocalAuth.js";
12
11
  export * as kysely from "./PublicKysely.js";
13
12
  export type { InferRow, Query, QueryRows, Row } from "./Query.js";
14
- export { formatValidMutationSizeError } from "./Schema.js";
15
- export type { EvoluSchema, ValidMutationSizeError } from "./Schema.js";
13
+ export type { EvoluSchema } from "./Schema.js";
16
14
  export type {
17
15
  NetworkError,
18
16
  PaymentRequiredError,
19
17
  ServerError,
18
+ SyncOwner,
20
19
  SyncState,
21
20
  SyncStateInitial,
22
21
  SyncStateIsNotSynced,
@@ -24,15 +23,14 @@ export type {
24
23
  SyncStateIsSyncing,
25
24
  } from "./Sync.js";
26
25
  export {
27
- binaryTimestampToTimestamp,
28
26
  Timestamp,
29
- timestampToBinaryTimestamp,
27
+ timestampBytesToTimestamp,
28
+ timestampToTimestampBytes,
30
29
  } from "./Timestamp.js";
31
30
  export type {
32
- BinaryTimestamp,
31
+ TimestampBytes,
33
32
  TimestampCounterOverflowError,
34
33
  TimestampDriftError,
35
- TimestampDuplicateNodeError,
36
34
  TimestampError,
37
35
  TimestampTimeOutOfRangeError,
38
36
  } from "./Timestamp.js";
@@ -65,7 +65,7 @@ export type { NotNull } from "kysely";
65
65
  * from "person"
66
66
  * ```
67
67
  */
68
- // SQL formatting breaks generated SQL for some reason.
68
+ // Kysely expects strict AST.
69
69
  // prettier-ignore
70
70
  export function jsonArrayFrom<O>(
71
71
  expr: SelectQueryBuilderExpression<O>,
@@ -122,7 +122,7 @@ export function jsonArrayFrom<O>(
122
122
  * from "person";
123
123
  * ```
124
124
  */
125
- // SQL formatting breaks generated SQL for some reason.
125
+ // Kysely expects strict AST.
126
126
  // prettier-ignore
127
127
  export function jsonObjectFrom<O>(
128
128
  expr: SelectQueryBuilderExpression<O>,
@@ -172,7 +172,7 @@ export function jsonObjectFrom<O>(
172
172
  * from "person"
173
173
  * ```
174
174
  */
175
- // SQL formatting breaks generated SQL for some reason.
175
+ // Kysely expects strict AST.
176
176
  // prettier-ignore
177
177
  export function jsonBuildObject<O extends Record<string, Expression<unknown>>>(
178
178
  obj: O,
@@ -4,14 +4,15 @@ import { TimingSafeEqualDep } from "../Crypto.js";
4
4
  import { err, ok, Result } from "../Result.js";
5
5
  import { sql, SqliteError } from "../Sqlite.js";
6
6
  import { SimpleName } from "../Type.js";
7
- import { OwnerId, WriteKey } from "./Owner.js";
8
- import { EncryptedDbChange, Storage } from "./Protocol.js";
7
+ import { OwnerId, OwnerWriteKey } from "./Owner.js";
9
8
  import {
10
9
  createSqliteStorageBase,
11
10
  CreateSqliteStorageBaseOptions,
11
+ EncryptedDbChange,
12
12
  SqliteStorageDeps,
13
+ Storage,
13
14
  } from "./Storage.js";
14
- import { timestampToBinaryTimestamp } from "./Timestamp.js";
15
+ import { timestampToTimestampBytes } from "./Timestamp.js";
15
16
 
16
17
  export interface Relay extends Disposable {}
17
18
 
@@ -55,7 +56,7 @@ export const createRelayStorage =
55
56
  ...sqliteStorageBase.value,
56
57
 
57
58
  /**
58
- * Lazily authorizes the initiator's {@link WriteKey} for the given
59
+ * Lazily authorizes the initiator's {@link OwnerWriteKey} for the given
59
60
  * {@link OwnerId}.
60
61
  *
61
62
  * - If the {@link OwnerId} does not exist, it is created and associated with
@@ -64,11 +65,13 @@ export const createRelayStorage =
64
65
  * stored key.
65
66
  */
66
67
  validateWriteKey: (ownerId, writeKey) => {
67
- const selectWriteKey = deps.sqlite.exec<{ writeKey: WriteKey }>(sql`
68
- select writeKey
69
- from evolu_writeKey
70
- where ownerId = ${ownerId};
71
- `);
68
+ const selectWriteKey = deps.sqlite.exec<{ writeKey: OwnerWriteKey }>(
69
+ sql`
70
+ select writeKey
71
+ from evolu_writeKey
72
+ where ownerId = ${ownerId};
73
+ `,
74
+ );
72
75
  if (!selectWriteKey.ok) {
73
76
  options.onStorageError(selectWriteKey.error);
74
77
  return false;
@@ -107,13 +110,15 @@ export const createRelayStorage =
107
110
  return true;
108
111
  },
109
112
 
110
- writeMessages: (ownerId, messages) => {
113
+ // https://eslint.org/docs/latest/rules/require-await#when-not-to-use-it
114
+ // eslint-disable-next-line @typescript-eslint/require-await
115
+ writeMessages: async (ownerId, messages) => {
111
116
  const result = deps.sqlite.transaction(() => {
112
117
  for (const message of messages) {
113
118
  const insertTimestampResult =
114
119
  sqliteStorageBase.value.insertTimestamp(
115
120
  ownerId,
116
- timestampToBinaryTimestamp(message.timestamp),
121
+ timestampToTimestampBytes(message.timestamp),
117
122
  );
118
123
  if (!insertTimestampResult.ok) return insertTimestampResult;
119
124
 
@@ -122,7 +127,7 @@ export const createRelayStorage =
122
127
  values
123
128
  (
124
129
  ${ownerId},
125
- ${timestampToBinaryTimestamp(message.timestamp)},
130
+ ${timestampToTimestampBytes(message.timestamp)},
126
131
  ${message.change}
127
132
  )
128
133
  on conflict do nothing;
@@ -1,20 +1,25 @@
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,
16
+ array,
12
17
  DateIso,
13
- DateIsoString,
14
- IdType,
18
+ IdBytes,
15
19
  InferErrors,
16
20
  InferInput,
17
21
  InferType,
22
+ maxMutationSize,
18
23
  MergeObjectTypeErrors,
19
24
  nullableToOptional,
20
25
  NullableToOptionalProps,
@@ -24,19 +29,18 @@ import {
24
29
  omit,
25
30
  optional,
26
31
  OptionalType,
32
+ String,
33
+ TableId,
27
34
  Type,
28
- TypeError,
35
+ ValidMutationSize,
36
+ validMutationSize,
37
+ ValidMutationSizeError,
29
38
  } from "../Type.js";
30
39
  import { Simplify } from "../Types.js";
31
- import { DbSchema } from "./Db.js";
32
- import { createIndexes, DbIndexesBuilder } from "./Kysely.js";
33
- import {
34
- BinaryId,
35
- CrdtMessage,
36
- maxProtocolMessageRangesSize,
37
- } from "./Protocol.js";
40
+ import { AppOwner, OwnerId } from "./Owner.js";
38
41
  import { Query, Row } from "./Query.js";
39
- import { BinaryTimestamp } from "./Timestamp.js";
42
+ import { CrdtMessage, DbChange } from "./Storage.js";
43
+ import { TimestampBytes } from "./Timestamp.js";
40
44
 
41
45
  /**
42
46
  * Defines the schema of an Evolu database.
@@ -75,7 +79,7 @@ import { BinaryTimestamp } from "./Timestamp.js";
75
79
  export type EvoluSchema = ReadonlyRecord<
76
80
  string,
77
81
  // TypeScript errors are cryptic so we use ValidateSchema.
78
- ReadonlyRecord<string, Type<any, any, any, any, any>>
82
+ ReadonlyRecord<string, Type<any, any, any, any, any, any>>
79
83
  >;
80
84
 
81
85
  /**
@@ -115,7 +119,7 @@ export type ValidateIdColumnType<S extends EvoluSchema> =
115
119
  keyof S extends infer TableName
116
120
  ? TableName extends keyof S
117
121
  ? "id" extends keyof S[TableName]
118
- ? S[TableName]["id"] extends IdType<any>
122
+ ? S[TableName]["id"] extends TableId<any>
119
123
  ? never
120
124
  : SchemaValidationError<`Table "${TableName & string}" id column must be a branded ID type (created with id("${TableName & string}")).`>
121
125
  : never
@@ -152,9 +156,13 @@ export type ValidateColumnTypes<S extends EvoluSchema> =
152
156
  export type SchemaValidationError<Message extends string> =
153
157
  `❌ Schema Error: ${Message}`;
154
158
 
159
+ export type IndexesConfig = (
160
+ create: (indexName: string) => Kysely.CreateIndexBuilder,
161
+ ) => ReadonlyArray<Kysely.CreateIndexBuilder<any>>;
162
+
155
163
  export const evoluSchemaToDbSchema = (
156
164
  schema: EvoluSchema,
157
- indexes?: DbIndexesBuilder,
165
+ indexesConfig?: IndexesConfig,
158
166
  ): DbSchema => {
159
167
  const tables = objectToEntries(schema).map(([tableName, table]) => ({
160
168
  name: tableName,
@@ -163,20 +171,22 @@ export const evoluSchemaToDbSchema = (
163
171
  .map(([k]) => k),
164
172
  }));
165
173
 
166
- const dbSchema = { tables, indexes: createIndexes(indexes) };
174
+ const indexes = indexesConfig
175
+ ? indexesConfig(createIndex).map(
176
+ (index): DbIndex => ({
177
+ name: index.toOperationNode().name.name,
178
+ sql: index.compile().sql,
179
+ }),
180
+ )
181
+ : [];
167
182
 
168
- assert(
169
- DbSchema.is(dbSchema),
170
- "Invalid EvoluSchema: Table and column names must use only characters A-Za-z0-9_- and be at most 256 characters long.",
171
- );
172
-
173
- return dbSchema;
183
+ return { tables, indexes };
174
184
  };
175
185
 
176
186
  export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
177
187
  queryCallback: (
178
188
  db: Pick<
179
- Kysely<
189
+ Kysely.Kysely<
180
190
  {
181
191
  [Table in keyof S]: {
182
192
  readonly [Column in keyof S[Table]]: Column extends
@@ -188,9 +198,9 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
188
198
  } & DefaultColumns;
189
199
  } & {
190
200
  readonly evolu_history: {
191
- readonly timestamp: BinaryTimestamp;
201
+ readonly timestamp: TimestampBytes;
192
202
  readonly table: keyof S;
193
- readonly id: BinaryId;
203
+ readonly id: IdBytes;
194
204
  readonly column: string;
195
205
  readonly value: SqliteValue;
196
206
  };
@@ -198,7 +208,7 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
198
208
  >,
199
209
  "selectFrom" | "fn" | "with" | "withRecursive"
200
210
  >,
201
- ) => SelectQueryBuilder<any, any, R>,
211
+ ) => Kysely.SelectQueryBuilder<any, any, R>,
202
212
  options?: SqliteQueryOptions,
203
213
  ) => Query<Simplify<R>>;
204
214
 
@@ -213,8 +223,8 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
213
223
  * - `isDeleted`: Soft delete flag.
214
224
  */
215
225
  export const DefaultColumns = object({
216
- createdAt: DateIsoString,
217
- updatedAt: DateIsoString,
226
+ createdAt: DateIso,
227
+ updatedAt: DateIso,
218
228
  isDeleted: nullOr(SqliteBoolean),
219
229
  });
220
230
  export type DefaultColumns = typeof DefaultColumns.Type;
@@ -243,7 +253,48 @@ export type MutationMapping<
243
253
  : UpsertableProps<P>;
244
254
 
245
255
  export interface MutationOptions {
256
+ /**
257
+ * Called after the mutation is completed and the local state is updated.
258
+ * Useful for triggering side effects (e.g., notifications, UI updates) after
259
+ * insert, update, or upsert.
260
+ */
246
261
  readonly onComplete?: () => void;
262
+
263
+ /**
264
+ * Specifies the owner ID for this mutation. If omitted, the default
265
+ * {@link AppOwner} is used.
266
+ *
267
+ * The owner must be used with `evolu.useOwner()` to enable sync. Mutations
268
+ * with unused owners are stored locally but not synced until the owner is
269
+ * used.
270
+ *
271
+ * ### Example
272
+ *
273
+ * ```ts
274
+ * // Partition your own data by project (derived from your AppOwner)
275
+ * const projectOwner = deriveShardOwner(appOwner, [
276
+ * "project",
277
+ * projectId,
278
+ * ]);
279
+ * evolu.insert(
280
+ * "task",
281
+ * { title: "Task 1" },
282
+ * { ownerId: projectOwner.id },
283
+ * );
284
+ *
285
+ * // Collaborative data (independent owner shared with others)
286
+ * const sharedOwner = createSharedOwner(sharedSecret);
287
+ * evolu.insert(
288
+ * "comment",
289
+ * { text: "Hello" },
290
+ * { ownerId: sharedOwner.id },
291
+ * );
292
+ * ```
293
+ *
294
+ * @experimental
295
+ */
296
+ readonly ownerId?: OwnerId;
297
+
247
298
  /**
248
299
  * Only validate, don't mutate.
249
300
  *
@@ -253,37 +304,10 @@ export interface MutationOptions {
253
304
  readonly onlyValidate?: boolean;
254
305
  }
255
306
 
256
- /**
257
- * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
258
- * the {@link maxProtocolMessageRangesSize}. The max size is 640KB in bytes,
259
- * measured via MessagePack. Evolu Protocol DbChange will be smaller thanks to
260
- * various optimizations.
261
- */
262
- export const maxMutationSize = 655360;
263
-
264
- const validMutationSize = <T extends AnyType>(type: T) =>
265
- brand("ValidMutationSize", type, (value) =>
266
- pack(value).byteLength <= maxMutationSize
267
- ? ok(value)
268
- : err<ValidMutationSizeError>({ type: "ValidMutationSize", value }),
269
- );
270
-
271
- export interface ValidMutationSizeError
272
- extends TypeError<"ValidMutationSize"> {}
273
-
274
- export const formatValidMutationSizeError =
275
- createTypeErrorFormatter<ValidMutationSizeError>(
276
- (error) =>
277
- `The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`,
278
- );
279
-
280
- export type ValidMutationSize<Props extends Record<string, AnyType>> =
281
- BrandType<
282
- ObjectType<Props>,
283
- "ValidMutationSize",
284
- ValidMutationSizeError,
285
- InferErrors<ObjectType<Props>>
286
- >;
307
+ export interface MutationChange extends DbChange {
308
+ /** Owner of the change. If undefined, the change belongs to the AppOwner. */
309
+ readonly ownerId?: OwnerId | undefined;
310
+ }
287
311
 
288
312
  /**
289
313
  * Type Factory to create insertable {@link Type}. It makes nullable Types
@@ -410,3 +434,184 @@ export type InferColumnErrors<
410
434
  MutationMapping<T, M>[Column]
411
435
  >;
412
436
  }[keyof MutationMapping<T, M>];
437
+
438
+ export const DbTable = object({
439
+ name: String,
440
+ columns: array(String),
441
+ });
442
+ export type DbTable = typeof DbTable.Type;
443
+
444
+ export const DbIndex = object({ name: String, sql: String });
445
+ export type DbIndex = typeof DbIndex.Type;
446
+
447
+ export const DbSchema = object({
448
+ tables: array(DbTable),
449
+ indexes: array(DbIndex),
450
+ });
451
+ export type DbSchema = typeof DbSchema.Type;
452
+
453
+ /** Get the current database schema by reading SQLite metadata. */
454
+ export const getDbSchema =
455
+ (deps: SqliteDep) =>
456
+ ({ allIndexes = false }: { allIndexes?: boolean } = {}): Result<
457
+ DbSchema,
458
+ SqliteError
459
+ > => {
460
+ const map = new Map<string, Array<string>>();
461
+
462
+ const tableAndColumnInfoRows = deps.sqlite.exec(sql`
463
+ select
464
+ sqlite_master.name as tableName,
465
+ table_info.name as columnName
466
+ from
467
+ sqlite_master
468
+ join pragma_table_info(sqlite_master.name) as table_info;
469
+ `);
470
+
471
+ if (!tableAndColumnInfoRows.ok) return tableAndColumnInfoRows;
472
+
473
+ tableAndColumnInfoRows.value.rows.forEach((row) => {
474
+ const { tableName, columnName } = row as unknown as {
475
+ tableName: string;
476
+ columnName: string;
477
+ };
478
+ if (!map.has(tableName)) map.set(tableName, []);
479
+ map.get(tableName)?.push(columnName);
480
+ });
481
+
482
+ const tables = Array.from(map, ([name, columns]) => ({ name, columns }));
483
+
484
+ const indexesRows = deps.sqlite.exec(
485
+ allIndexes
486
+ ? sql`
487
+ select name, sql
488
+ from sqlite_master
489
+ where type = 'index' and name not like 'sqlite_%';
490
+ `
491
+ : sql`
492
+ select name, sql
493
+ from sqlite_master
494
+ where
495
+ type = 'index'
496
+ and name not like 'sqlite_%'
497
+ and name not like 'evolu_%';
498
+ `,
499
+ );
500
+
501
+ if (!indexesRows.ok) return indexesRows;
502
+
503
+ const indexes = indexesRows.value.rows.map(
504
+ (row): DbIndex => ({
505
+ name: row.name as string,
506
+ /**
507
+ * SQLite returns "CREATE INDEX" for "create index" for some reason.
508
+ * Other keywords remain unchanged. We have to normalize the casing for
509
+ * {@link indexesAreEqual} manually.
510
+ */
511
+ sql: (row.sql as string)
512
+ .replace("CREATE INDEX", "create index")
513
+ .replace("CREATE UNIQUE INDEX", "create unique index"),
514
+ }),
515
+ );
516
+
517
+ return ok({ tables, indexes });
518
+ };
519
+
520
+ const indexesAreEqual = (self: DbIndex, that: DbIndex): boolean =>
521
+ self.name === that.name && self.sql === that.sql;
522
+
523
+ export const ensureDbSchema =
524
+ (deps: SqliteDep) =>
525
+ (
526
+ newSchema: DbSchema,
527
+ currentSchema: DbSchema,
528
+ options?: { ignoreIndexes: boolean },
529
+ ): Result<void, SqliteError> => {
530
+ const queries: Array<SqliteQuery> = [];
531
+
532
+ newSchema.tables.forEach((newTable) => {
533
+ const currentTable = currentSchema.tables.find(
534
+ (t) => t.name === newTable.name,
535
+ );
536
+ if (!currentTable) {
537
+ queries.push({
538
+ sql: createTableWithDefaultColumns(newTable.name, newTable.columns),
539
+ parameters: [],
540
+ });
541
+ } else {
542
+ newTable.columns
543
+ .filter((newColumn) => !currentTable.columns.includes(newColumn))
544
+ .forEach((newColumn) => {
545
+ queries.push(sql`
546
+ alter table ${sql.identifier(newTable.name)}
547
+ add column ${sql.identifier(newColumn)} blob;
548
+ `);
549
+ });
550
+ }
551
+ });
552
+
553
+ if (options?.ignoreIndexes !== true) {
554
+ // Remove current indexes that are not in the newSchema.
555
+ currentSchema.indexes
556
+ .filter(
557
+ (currentIndex) =>
558
+ !newSchema.indexes.some((newIndex) =>
559
+ indexesAreEqual(newIndex, currentIndex),
560
+ ),
561
+ )
562
+ .forEach((index) => {
563
+ queries.push(sql`drop index ${sql.identifier(index.name)};`);
564
+ });
565
+
566
+ // Add new indexes that are not in the currentSchema.
567
+ newSchema.indexes
568
+ .filter(
569
+ (newIndex) =>
570
+ !currentSchema.indexes.some((currentIndex) =>
571
+ indexesAreEqual(newIndex, currentIndex),
572
+ ),
573
+ )
574
+ .forEach((newIndex) => {
575
+ queries.push({ sql: `${newIndex.sql};` as SafeSql, parameters: [] });
576
+ });
577
+ }
578
+
579
+ for (const query of queries) {
580
+ const result = deps.sqlite.exec(query);
581
+ if (!result.ok) return result;
582
+ }
583
+ return ok();
584
+ };
585
+
586
+ const createTableWithDefaultColumns = (
587
+ tableName: string,
588
+ columns: ReadonlyArray<string>,
589
+ ): SafeSql =>
590
+ `
591
+ create table ${sql.identifier(tableName).sql} (
592
+ "id" text primary key,
593
+ ${columns
594
+ // Add default columns.
595
+ .concat(["createdAt", "updatedAt", "isDeleted"])
596
+ .filter((c) => c !== "id")
597
+ // "A column with affinity BLOB does not prefer one storage class over another
598
+ // and no attempt is made to coerce data from one storage class into another."
599
+ // https://www.sqlite.org/datatype3.html
600
+ .map((name) => `${sql.identifier(name).sql} blob`)
601
+ .join(", ")}
602
+ );
603
+ ` as SafeSql;
604
+
605
+ // https://kysely.dev/docs/recipes/splitting-query-building-and-execution
606
+ export const kysely = new Kysely.Kysely({
607
+ dialect: {
608
+ createAdapter: () => new Kysely.SqliteAdapter(),
609
+ createDriver: () => new Kysely.DummyDriver(),
610
+ createIntrospector() {
611
+ throw new Error("Not implemeneted");
612
+ },
613
+ createQueryCompiler: () => new Kysely.SqliteQueryCompiler(),
614
+ },
615
+ });
616
+
617
+ const createIndex = kysely.schema.createIndex.bind(kysely.schema);