@evolu/common 6.0.1-preview.32 → 6.0.1-preview.34

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 (49) hide show
  1. package/README.md +2 -2
  2. package/dist/src/Array.d.ts +193 -41
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +155 -48
  5. package/dist/src/Crypto.d.ts +6 -8
  6. package/dist/src/Crypto.d.ts.map +1 -1
  7. package/dist/src/Crypto.js +9 -9
  8. package/dist/src/Evolu/Db.d.ts.map +1 -1
  9. package/dist/src/Evolu/Db.js +4 -4
  10. package/dist/src/Evolu/Evolu.d.ts +9 -4
  11. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  12. package/dist/src/Evolu/Evolu.js +11 -11
  13. package/dist/src/Evolu/Protocol.d.ts +24 -1
  14. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  15. package/dist/src/Evolu/Protocol.js +59 -30
  16. package/dist/src/Evolu/Query.d.ts +1 -1
  17. package/dist/src/Evolu/Query.d.ts.map +1 -1
  18. package/dist/src/Evolu/Query.js +1 -1
  19. package/dist/src/Evolu/Schema.d.ts +24 -20
  20. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  21. package/dist/src/Evolu/Schema.js +36 -27
  22. package/dist/src/Evolu/Storage.d.ts +10 -2
  23. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  24. package/dist/src/Evolu/Storage.js +18 -4
  25. package/dist/src/Evolu/Sync.d.ts +2 -1
  26. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  27. package/dist/src/Evolu/Sync.js +45 -33
  28. package/dist/src/Evolu/Timestamp.d.ts +12 -17
  29. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  30. package/dist/src/Evolu/Timestamp.js +5 -19
  31. package/dist/src/Object.d.ts +10 -4
  32. package/dist/src/Object.d.ts.map +1 -1
  33. package/dist/src/Object.js +9 -3
  34. package/dist/src/Type.d.ts +57 -11
  35. package/dist/src/Type.d.ts.map +1 -1
  36. package/dist/src/Type.js +58 -28
  37. package/package.json +2 -2
  38. package/src/Array.ts +213 -55
  39. package/src/Crypto.ts +11 -9
  40. package/src/Evolu/Db.ts +7 -7
  41. package/src/Evolu/Evolu.ts +21 -20
  42. package/src/Evolu/Protocol.ts +70 -35
  43. package/src/Evolu/Query.ts +2 -2
  44. package/src/Evolu/Schema.ts +44 -32
  45. package/src/Evolu/Storage.ts +40 -2
  46. package/src/Evolu/Sync.ts +54 -32
  47. package/src/Evolu/Timestamp.ts +7 -28
  48. package/src/Object.ts +13 -5
  49. package/src/Type.ts +58 -32
@@ -171,6 +171,8 @@
171
171
  * `applyProtocolMessage` function with conditional arguments to reduce code
172
172
  * duplication.
173
173
  * - ProtocolQuotaError should return storedBytes and actual quota.
174
+ * - Replace try-catch with Result + new Error (to preserve stacktraces). Measure
175
+ * Result overhead, it should be super small.
174
176
  */
175
177
 
176
178
  import { Packr } from "msgpackr";
@@ -186,8 +188,8 @@ import {
186
188
  utf8ToBytes,
187
189
  } from "../Buffer.js";
188
190
  import {
191
+ createPadmePadding,
189
192
  EncryptionKey,
190
- padmePaddingLength,
191
193
  RandomBytesDep,
192
194
  SymmetricCryptoDecryptError,
193
195
  SymmetricCryptoDep,
@@ -436,7 +438,7 @@ export interface ProtocolSyncError extends BaseOwnerError {
436
438
  export interface ProtocolTimestampMismatchError {
437
439
  readonly type: "ProtocolTimestampMismatchError";
438
440
  readonly expected: Timestamp;
439
- readonly embedded: Timestamp;
441
+ readonly timestamp: Timestamp;
440
442
  }
441
443
 
442
444
  /**
@@ -909,8 +911,6 @@ export const applyProtocolMessageAsClient =
909
911
  | ProtocolQuotaError
910
912
  >
911
913
  > => {
912
- // try-catch instead of Result for performance and stacktraces
913
- // DEV: Measure it again, I think we should use Result with new Error.
914
914
  try {
915
915
  const input = createBuffer(inputMessage);
916
916
  const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
@@ -1057,8 +1057,6 @@ export const applyProtocolMessageAsRelay =
1057
1057
  ): Promise<
1058
1058
  Result<ApplyProtocolMessageAsRelayResult, ProtocolInvalidDataError>
1059
1059
  > => {
1060
- // try-catch instead of Result for performance and stacktraces
1061
- // DEV: Measure it again, I think we should use Result with new Error.
1062
1060
  try {
1063
1061
  const input = createBuffer(inputMessage);
1064
1062
  const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
@@ -1664,6 +1662,52 @@ export const decodeNumber = (buffer: Buffer): number => {
1664
1662
  return numberResult.value;
1665
1663
  };
1666
1664
 
1665
+ /**
1666
+ * Encodes an array of boolean flags into a single byte.
1667
+ *
1668
+ * Each element in the array corresponds to a bit (0-7). Array can have 0-8
1669
+ * elements.
1670
+ *
1671
+ * ### Example
1672
+ *
1673
+ * ```ts
1674
+ * encodeFlags(buffer, [true, false, true]); // Encodes bits 0, 1, 2
1675
+ * ```
1676
+ */
1677
+ export const encodeFlags = (
1678
+ buffer: Buffer,
1679
+ flags: ReadonlyArray<boolean>,
1680
+ ): void => {
1681
+ let byte = 0;
1682
+ for (let i = 0; i < flags.length && i < 8; i++) {
1683
+ if (flags[i]) {
1684
+ byte |= 1 << i;
1685
+ }
1686
+ }
1687
+ buffer.extend([byte]);
1688
+ };
1689
+
1690
+ /**
1691
+ * Decodes a byte into an array of boolean flags.
1692
+ *
1693
+ * ### Example
1694
+ *
1695
+ * ```ts
1696
+ * const flags = decodeFlags(buffer, 3); // Decode 3 flags
1697
+ * ```
1698
+ */
1699
+ export const decodeFlags = (
1700
+ buffer: Buffer,
1701
+ count: PositiveInt,
1702
+ ): ReadonlyArray<boolean> => {
1703
+ const byte = buffer.shift();
1704
+ const flags: Array<boolean> = [];
1705
+ for (let i = 0; i < count && i < 8; i++) {
1706
+ flags.push((byte & (1 << i)) !== 0);
1707
+ }
1708
+ return flags;
1709
+ };
1710
+
1667
1711
  /**
1668
1712
  * Encodes and encrypts a {@link DbChange} using the provided owner's encryption
1669
1713
  * key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
@@ -1675,36 +1719,29 @@ export const decodeNumber = (buffer: Buffer): number => {
1675
1719
  export const encodeAndEncryptDbChange =
1676
1720
  (deps: SymmetricCryptoDep) =>
1677
1721
  (message: CrdtMessage, key: EncryptionKey): EncryptedDbChange => {
1678
- const change = message.change;
1679
1722
  const buffer = createBuffer();
1680
1723
 
1681
- // Encode protocol version first for backward compatibility
1682
1724
  encodeNonNegativeInt(buffer, protocolVersion);
1683
1725
 
1684
- // Encode the timestamp (after version) for tamper verification
1685
- const timestampBytes = timestampToTimestampBytes(message.timestamp);
1686
- buffer.extend(timestampBytes);
1726
+ // Encode the timestamp to prevent tampering (e.g., a malicious relay
1727
+ // assigning this EncryptedDbChange to a different EncryptedCrdtMessage)
1728
+ buffer.extend(timestampToTimestampBytes(message.timestamp));
1687
1729
 
1688
- encodeString(buffer, change.table);
1730
+ encodeFlags(buffer, [message.change.isInsert]);
1689
1731
 
1690
- buffer.extend(idToIdBytes(change.id));
1732
+ encodeString(buffer, message.change.table);
1733
+ buffer.extend(idToIdBytes(message.change.id));
1691
1734
 
1692
- const entries = objectToEntries(change.values).map(
1693
- ([column, value]): [string, SqliteValue] => {
1694
- return [column, value];
1695
- },
1696
- );
1735
+ const entries = objectToEntries(message.change.values);
1697
1736
 
1698
1737
  encodeLength(buffer, entries);
1699
-
1700
1738
  for (const [column, value] of entries) {
1701
1739
  encodeString(buffer, column);
1702
1740
  encodeSqliteValue(buffer, value);
1703
1741
  }
1704
1742
 
1705
- const paddingLength = padmePaddingLength(buffer.getLength());
1706
- // Add zero bytes as PADMÉ padding - these will be ignored during decoding.
1707
- buffer.extend(new Uint8Array(paddingLength));
1743
+ // Add PADMÉ padding (ignored during decoding)
1744
+ buffer.extend(createPadmePadding(buffer.getLength()));
1708
1745
 
1709
1746
  const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(
1710
1747
  buffer.unwrap(),
@@ -1735,13 +1772,11 @@ export const decryptAndDecodeDbChange =
1735
1772
  | ProtocolInvalidDataError
1736
1773
  | ProtocolTimestampMismatchError
1737
1774
  > => {
1738
- // try-catch instead of Result for performance and stacktraces
1739
1775
  try {
1740
1776
  const buffer = createBuffer(message.change);
1741
- const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
1742
1777
 
1743
- const ciphertextLength = decodeLength(buffer);
1744
- const ciphertext = buffer.shiftN(ciphertextLength);
1778
+ const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
1779
+ const ciphertext = buffer.shiftN(decodeLength(buffer));
1745
1780
 
1746
1781
  const plaintextBytes = deps.symmetricCrypto.decrypt(
1747
1782
  ciphertext,
@@ -1753,24 +1788,24 @@ export const decryptAndDecodeDbChange =
1753
1788
  buffer.reset();
1754
1789
  buffer.extend(plaintextBytes.value);
1755
1790
 
1756
- // Decode version (for future compatibility, no validation needed for now)
1791
+ // Decode version (for future compatibility, not need yet)
1757
1792
  decodeNonNegativeInt(buffer);
1758
1793
 
1759
- // Decode and verify the embedded timestamp
1760
- const embeddedTimestampBytes = buffer.shiftN(timestampBytesLength);
1761
- const embeddedTimestamp = timestampBytesToTimestamp(
1762
- embeddedTimestampBytes as TimestampBytes,
1794
+ const timestamp = timestampBytesToTimestamp(
1795
+ buffer.shiftN(timestampBytesLength) as TimestampBytes,
1763
1796
  );
1764
1797
 
1765
- // Verify timestamp integrity
1766
- if (!eqTimestamp(embeddedTimestamp, message.timestamp)) {
1798
+ if (!eqTimestamp(timestamp, message.timestamp)) {
1767
1799
  return err<ProtocolTimestampMismatchError>({
1768
1800
  type: "ProtocolTimestampMismatchError",
1769
1801
  expected: message.timestamp,
1770
- embedded: embeddedTimestamp,
1802
+ timestamp,
1771
1803
  });
1772
1804
  }
1773
1805
 
1806
+ const flags = decodeFlags(buffer, PositiveInt.orThrow(1));
1807
+ const isInsert = flags[0];
1808
+
1774
1809
  const table = decodeString(buffer);
1775
1810
  const id = decodeId(buffer);
1776
1811
 
@@ -1783,7 +1818,7 @@ export const decryptAndDecodeDbChange =
1783
1818
  values[column] = value;
1784
1819
  }
1785
1820
 
1786
- const dbChange = { table, id, values };
1821
+ const dbChange = DbChange.orThrow({ table, id, values, isInsert });
1787
1822
 
1788
1823
  return ok(dbChange);
1789
1824
  } catch (error) {
@@ -1,3 +1,4 @@
1
+ import { Brand } from "../Brand.js";
1
2
  import { bytesToHex, hexToBytes } from "../Buffer.js";
2
3
  import { objectToEntries } from "../Object.js";
3
4
  import {
@@ -9,7 +10,6 @@ import {
9
10
  } from "../Sqlite.js";
10
11
  import { Store, StoreSubscribe } from "../Store.js";
11
12
  import { Simplify } from "../Types.js";
12
- import { Brand } from "../Brand.js";
13
13
 
14
14
  /**
15
15
  * A type-safe SQL query.
@@ -52,7 +52,7 @@ export const serializeQuery = <R extends Row>(query: SqliteQuery): Query<R> => {
52
52
  );
53
53
 
54
54
  const options = query.options
55
- ? objectToEntries(query.options).sort(([a], [b]) => a.localeCompare(b))
55
+ ? objectToEntries(query.options).toSorted(([a], [b]) => a.localeCompare(b))
56
56
  : [];
57
57
 
58
58
  return JSON.stringify([query.sql, params, options]) as Query<R>;
@@ -1,4 +1,6 @@
1
1
  import * as Kysely from "kysely";
2
+ import { assert } from "../Assert.js";
3
+ import { createEqArrayLike, eqString } from "../Eq.js";
2
4
  import { mapObject, objectToEntries, ReadonlyRecord } from "../Object.js";
3
5
  import { ok, Result } from "../Result.js";
4
6
  import {
@@ -14,6 +16,7 @@ import {
14
16
  import {
15
17
  AnyType,
16
18
  array,
19
+ createIdFromString,
17
20
  DateIso,
18
21
  IdBytes,
19
22
  InferErrors,
@@ -40,7 +43,7 @@ import { Simplify } from "../Types.js";
40
43
  import { AppOwner, OwnerId } from "./Owner.js";
41
44
  import { Query, Row } from "./Query.js";
42
45
  import { CrdtMessage, DbChange } from "./Storage.js";
43
- import { TimestampBytes } from "./Timestamp.js";
46
+ import { Timestamp, TimestampBytes } from "./Timestamp.js";
44
47
 
45
48
  /**
46
49
  * Defines the schema of an Evolu database.
@@ -92,7 +95,7 @@ export type EvoluSchema = ReadonlyRecord<
92
95
  *
93
96
  * 1. All tables must have an 'id' column
94
97
  * 2. The 'id' column must be a branded ID type (created with id() function)
95
- * 3. Tables cannot use default column names (createdAt, updatedAt, isDeleted)
98
+ * 3. Tables cannot use system column names (createdAt, updatedAt, isDeleted)
96
99
  * 4. All column types must be compatible with SQLite (extend SqliteValue)
97
100
  */
98
101
  export type ValidateSchema<S extends EvoluSchema> =
@@ -132,7 +135,7 @@ export type ValidateNoDefaultColumns<S extends EvoluSchema> =
132
135
  ? keyof S[TableName] extends infer ColumnName
133
136
  ? ColumnName extends keyof S[TableName]
134
137
  ? ColumnName extends "createdAt" | "updatedAt" | "isDeleted"
135
- ? SchemaValidationError<`Table "${TableName & string}" uses default column name "${ColumnName & string}". Default columns (createdAt, updatedAt, isDeleted) are added automatically.`>
138
+ ? SchemaValidationError<`Table "${TableName & string}" uses system column name "${ColumnName & string}". System columns (createdAt, updatedAt, isDeleted) are added automatically.`>
136
139
  : never
137
140
  : never
138
141
  : never
@@ -195,7 +198,7 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
195
198
  | "updatedAt"
196
199
  ? InferType<S[Table][Column]>
197
200
  : InferType<S[Table][Column]> | null;
198
- } & DefaultColumns;
201
+ } & SystemColumns;
199
202
  } & {
200
203
  readonly evolu_history: {
201
204
  readonly timestamp: TimestampBytes;
@@ -213,21 +216,30 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
213
216
  ) => Query<Simplify<R>>;
214
217
 
215
218
  /**
216
- * Default columns automatically added to all tables.
219
+ * System columns that are implicitly defined by Evolu.
217
220
  *
218
- * - `createdAt`: Set by Evolu when `insert` is called, or can be custom with
219
- * `upsert`.
220
- * - `updatedAt`: Always set by Evolu, derived from {@link CrdtMessage} timestamp.
221
- * If you defer sync to avoid leaking time activity, use a custom column to
222
- * preserve real update time.
223
- * - `isDeleted`: Soft delete flag.
221
+ * - `createdAt`: Set by Evolu on row creation, derived from {@link Timestamp}.
222
+ * - `updatedAt`: Set by Evolu on every row change, derived from {@link Timestamp}.
223
+ * - `isDeleted`: Soft delete flag created by Evolu and used by the developer to
224
+ * mark rows as deleted.
224
225
  */
225
- export const DefaultColumns = object({
226
+ export const SystemColumns = object({
226
227
  createdAt: DateIso,
227
228
  updatedAt: DateIso,
228
229
  isDeleted: nullOr(SqliteBoolean),
230
+ // TODO: ownerId
229
231
  });
230
- export type DefaultColumns = typeof DefaultColumns.Type;
232
+ export type SystemColumns = typeof SystemColumns.Type;
233
+
234
+ export const systemColumns = ["createdAt", "updatedAt", "isDeleted"];
235
+
236
+ assert(
237
+ createEqArrayLike(eqString)(
238
+ objectToEntries(SystemColumns.props).map(([key]) => key),
239
+ systemColumns,
240
+ ),
241
+ "SystemColumns keys must match systemColumnsKeys",
242
+ );
231
243
 
232
244
  export type MutationKind = "insert" | "update" | "upsert";
233
245
 
@@ -311,7 +323,8 @@ export interface MutationChange extends DbChange {
311
323
 
312
324
  /**
313
325
  * Type Factory to create insertable {@link Type}. It makes nullable Types
314
- * optional, omits Id, and ensures the {@link maxMutationSize}.
326
+ * optional (so they are not required), omits Id, and ensures the
327
+ * {@link maxMutationSize}.
315
328
  *
316
329
  * ### Example
317
330
  *
@@ -341,7 +354,7 @@ export type Insertable<Props extends Record<string, AnyType>> = InferInput<
341
354
 
342
355
  /**
343
356
  * Type Factory to create updateable {@link Type}. It makes everything except for
344
- * the `id` column partial (i.e. optional) and ensures the
357
+ * the `id` column optional (so they are not required) and ensures the
345
358
  * {@link maxMutationSize}.
346
359
  *
347
360
  * ### Example
@@ -377,9 +390,15 @@ export type Updateable<Props extends Record<string, AnyType>> = InferInput<
377
390
  >;
378
391
 
379
392
  /**
380
- * Type Factory to create upsertable Type. It makes nullable Types optional,
381
- * includes optional default columns (createdAt, isDeleted), and ensures the
382
- * {@link maxMutationSize}.
393
+ * Type Factory to create an upsertable Type. It makes nullable Types optional
394
+ * (so they are not required) and ensures the {@link maxMutationSize}.
395
+ *
396
+ * Upsert is like insert, except it requires an ID. It's useful for inserting
397
+ * rows with external ID via {@link createIdFromString}.
398
+ *
399
+ * Note that it's not possible to upsert a row with `createdAt` nor `updatedAt`,
400
+ * because they are derived from {@link CrdtMessage} timestamp. For external
401
+ * createdAt, use a different column.
383
402
  *
384
403
  * ### Example
385
404
  *
@@ -389,7 +408,6 @@ export type Updateable<Props extends Record<string, AnyType>> = InferInput<
389
408
  * const todo = UpsertableTodo.from({
390
409
  * id,
391
410
  * title,
392
- * createdAt: "2023-01-01T00:00:00.000Z",
393
411
  * });
394
412
  * if (!todo.ok) return; // handle errors
395
413
  * ```
@@ -399,7 +417,6 @@ export const upsertable = <Props extends Record<string, AnyType>>(
399
417
  ): ValidMutationSize<UpsertableProps<Props>> => {
400
418
  const propsWithDefaults = {
401
419
  ...props,
402
- createdAt: optional(DateIso),
403
420
  isDeleted: optional(SqliteBoolean),
404
421
  };
405
422
  return validMutationSize(nullableToOptional(propsWithDefaults));
@@ -408,7 +425,6 @@ export const upsertable = <Props extends Record<string, AnyType>>(
408
425
  export type UpsertableProps<Props extends Record<string, AnyType>> =
409
426
  NullableToOptionalProps<
410
427
  Props & {
411
- createdAt: OptionalType<typeof DateIso>;
412
428
  isDeleted: OptionalType<typeof SqliteBoolean>;
413
429
  }
414
430
  >;
@@ -535,7 +551,7 @@ export const ensureDbSchema =
535
551
  );
536
552
  if (!currentTable) {
537
553
  queries.push({
538
- sql: createTableWithDefaultColumns(newTable.name, newTable.columns),
554
+ sql: createAppTableWithDefaultColumns(newTable),
539
555
  parameters: [],
540
556
  });
541
557
  } else {
@@ -583,16 +599,12 @@ export const ensureDbSchema =
583
599
  return ok();
584
600
  };
585
601
 
586
- const createTableWithDefaultColumns = (
587
- tableName: string,
588
- columns: ReadonlyArray<string>,
589
- ): SafeSql =>
590
- `
591
- create table ${sql.identifier(tableName).sql} (
602
+ const createAppTableWithDefaultColumns = (table: DbTable): SafeSql => {
603
+ return `
604
+ create table ${sql.identifier(table.name).sql} (
592
605
  "id" text primary key,
593
- ${columns
594
- // Add default columns.
595
- .concat(["createdAt", "updatedAt", "isDeleted"])
606
+ ${table.columns
607
+ .concat(systemColumns)
596
608
  .filter((c) => c !== "id")
597
609
  // "A column with affinity BLOB does not prefer one storage class over another
598
610
  // and no attempt is made to coerce data from one storage class into another."
@@ -601,7 +613,7 @@ const createTableWithDefaultColumns = (
601
613
  .join(", ")}
602
614
  );
603
615
  ` as SafeSql;
604
-
616
+ };
605
617
  // https://kysely.dev/docs/recipes/splitting-query-building-and-execution
606
618
  export const kysely = new Kysely.Kysely({
607
619
  dialect: {
@@ -1,5 +1,6 @@
1
1
  import { sha256 } from "@noble/hashes/sha2.js";
2
2
  import {
3
+ filterArray,
3
4
  firstInArray,
4
5
  isNonEmptyReadonlyArray,
5
6
  NonEmptyReadonlyArray,
@@ -9,10 +10,12 @@ import { Brand } from "../Brand.js";
9
10
  import { concatBytes } from "../Buffer.js";
10
11
  import { decrement } from "../Number.js";
11
12
  import { RandomDep } from "../Random.js";
12
- import { ok, Result } from "../Result.js";
13
+ import { err, ok, Result } from "../Result.js";
13
14
  import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
14
15
  import { MaybeAsync } from "../Task.js";
15
16
  import {
17
+ Boolean,
18
+ brand,
16
19
  Id,
17
20
  Int64String,
18
21
  NonNegativeInt,
@@ -20,6 +23,7 @@ import {
20
23
  PositiveInt,
21
24
  record,
22
25
  String,
26
+ TypeError,
23
27
  } from "../Type.js";
24
28
  import {
25
29
  BaseOwnerError,
@@ -29,6 +33,7 @@ import {
29
33
  OwnerWriteKey,
30
34
  } from "./Owner.js";
31
35
  import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
36
+ import { systemColumns } from "./Schema.js";
32
37
 
33
38
  export interface StorageConfig {
34
39
  /**
@@ -248,6 +253,38 @@ export interface CrdtMessage {
248
253
  readonly change: DbChange;
249
254
  }
250
255
 
256
+ export const DbChangeValues = record(String, SqliteValue);
257
+ export type DbChangeValues = typeof DbChangeValues.Type;
258
+
259
+ const systemColumnsWithIdAndWithoutIsDeleted = filterArray(
260
+ systemColumns.concat("id"),
261
+ (column) => column != "isDeleted",
262
+ );
263
+
264
+ export const ValidDbChangeValues = brand(
265
+ "ValidDbChangeValues",
266
+ DbChangeValues,
267
+ (value) => {
268
+ const invalidColumns = systemColumnsWithIdAndWithoutIsDeleted.filter(
269
+ (key) => key in value,
270
+ );
271
+ if (invalidColumns.length > 0)
272
+ return err<ValidDbChangeValuesError>({
273
+ type: "ValidDbChangeValues",
274
+ value,
275
+ invalidColumns,
276
+ });
277
+
278
+ return ok(value);
279
+ },
280
+ );
281
+ export type ValidDbChangeValues = typeof ValidDbChangeValues.Type;
282
+
283
+ export interface ValidDbChangeValuesError
284
+ extends TypeError<"ValidDbChangeValues"> {
285
+ readonly invalidColumns: ReadonlyArray<string>;
286
+ }
287
+
251
288
  /**
252
289
  * A DbChange is a change to a table row. Together with a unique
253
290
  * {@link Timestamp}, it forms a {@link CrdtMessage}.
@@ -255,7 +292,8 @@ export interface CrdtMessage {
255
292
  export const DbChange = object({
256
293
  table: String,
257
294
  id: Id,
258
- values: record(String, SqliteValue),
295
+ values: ValidDbChangeValues,
296
+ isInsert: Boolean,
259
297
  });
260
298
  export type DbChange = typeof DbChange.Type;
261
299