@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.
- package/README.md +2 -2
- package/dist/src/Array.d.ts +193 -41
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +155 -48
- package/dist/src/Crypto.d.ts +6 -8
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +9 -9
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +4 -4
- package/dist/src/Evolu/Evolu.d.ts +9 -4
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +11 -11
- package/dist/src/Evolu/Protocol.d.ts +24 -1
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +59 -30
- package/dist/src/Evolu/Query.d.ts +1 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Query.js +1 -1
- package/dist/src/Evolu/Schema.d.ts +24 -20
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +36 -27
- package/dist/src/Evolu/Storage.d.ts +10 -2
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +18 -4
- package/dist/src/Evolu/Sync.d.ts +2 -1
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +45 -33
- package/dist/src/Evolu/Timestamp.d.ts +12 -17
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +5 -19
- package/dist/src/Object.d.ts +10 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +9 -3
- package/dist/src/Type.d.ts +57 -11
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +58 -28
- package/package.json +2 -2
- package/src/Array.ts +213 -55
- package/src/Crypto.ts +11 -9
- package/src/Evolu/Db.ts +7 -7
- package/src/Evolu/Evolu.ts +21 -20
- package/src/Evolu/Protocol.ts +70 -35
- package/src/Evolu/Query.ts +2 -2
- package/src/Evolu/Schema.ts +44 -32
- package/src/Evolu/Storage.ts +40 -2
- package/src/Evolu/Sync.ts +54 -32
- package/src/Evolu/Timestamp.ts +7 -28
- package/src/Object.ts +13 -5
- package/src/Type.ts +58 -32
package/src/Evolu/Protocol.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
1685
|
-
|
|
1686
|
-
buffer.extend(
|
|
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
|
-
|
|
1730
|
+
encodeFlags(buffer, [message.change.isInsert]);
|
|
1689
1731
|
|
|
1690
|
-
buffer.
|
|
1732
|
+
encodeString(buffer, message.change.table);
|
|
1733
|
+
buffer.extend(idToIdBytes(message.change.id));
|
|
1691
1734
|
|
|
1692
|
-
const entries = objectToEntries(change.values)
|
|
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
|
-
|
|
1706
|
-
|
|
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
|
|
1744
|
-
const ciphertext = buffer.shiftN(
|
|
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,
|
|
1791
|
+
// Decode version (for future compatibility, not need yet)
|
|
1757
1792
|
decodeNonNegativeInt(buffer);
|
|
1758
1793
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
const embeddedTimestamp = timestampBytesToTimestamp(
|
|
1762
|
-
embeddedTimestampBytes as TimestampBytes,
|
|
1794
|
+
const timestamp = timestampBytesToTimestamp(
|
|
1795
|
+
buffer.shiftN(timestampBytesLength) as TimestampBytes,
|
|
1763
1796
|
);
|
|
1764
1797
|
|
|
1765
|
-
|
|
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
|
-
|
|
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) {
|
package/src/Evolu/Query.ts
CHANGED
|
@@ -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).
|
|
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>;
|
package/src/Evolu/Schema.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
} &
|
|
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
|
-
*
|
|
219
|
+
* System columns that are implicitly defined by Evolu.
|
|
217
220
|
*
|
|
218
|
-
* - `createdAt`: Set by Evolu
|
|
219
|
-
*
|
|
220
|
-
* - `
|
|
221
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
382
|
-
*
|
|
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:
|
|
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
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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: {
|
package/src/Evolu/Storage.ts
CHANGED
|
@@ -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:
|
|
295
|
+
values: ValidDbChangeValues,
|
|
296
|
+
isInsert: Boolean,
|
|
259
297
|
});
|
|
260
298
|
export type DbChange = typeof DbChange.Type;
|
|
261
299
|
|