@evolu/common 6.0.1-preview.12 → 6.0.1-preview.14
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/dist/src/Evolu/Db.d.ts +1 -6
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +17 -64
- package/dist/src/Evolu/Evolu.d.ts +126 -6
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +18 -4
- package/dist/src/Evolu/Protocol.d.ts +20 -5
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +32 -8
- package/dist/src/Evolu/Schema.d.ts +23 -5
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +27 -5
- package/dist/src/Evolu/Timestamp.d.ts +6 -0
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +7 -0
- package/dist/src/Type.d.ts +5 -1
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +5 -1
- package/package.json +3 -2
- package/src/Evolu/Db.ts +17 -100
- package/src/Evolu/Evolu.ts +148 -10
- package/src/Evolu/Protocol.ts +90 -38
- package/src/Evolu/Schema.ts +38 -7
- package/src/Evolu/Timestamp.ts +8 -0
- package/src/Type.ts +5 -1
package/src/Evolu/Evolu.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from "./Query.js";
|
|
46
46
|
import {
|
|
47
47
|
CreateQuery,
|
|
48
|
+
DefaultColumns,
|
|
48
49
|
EvoluSchema,
|
|
49
50
|
evoluSchemaToDbSchema,
|
|
50
51
|
insertable,
|
|
@@ -253,34 +254,154 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
|
|
|
253
254
|
readonly getSyncState: () => SyncState;
|
|
254
255
|
|
|
255
256
|
/**
|
|
256
|
-
* Inserts a row
|
|
257
|
+
* Inserts a row into the database and returns a {@link Result} with the new
|
|
258
|
+
* {@link Id}.
|
|
259
|
+
*
|
|
260
|
+
* The first argument is the table name, and the second is an object
|
|
261
|
+
* containing the row data. An optional third argument provides mutation
|
|
262
|
+
* options including an `onComplete` callback and `onlyValidate` flag.
|
|
263
|
+
*
|
|
264
|
+
* Returns a Result type - use `.ok` to check if the insertion succeeded, and
|
|
265
|
+
* `.value.id` to access the generated ID on success, or `.error` to handle
|
|
266
|
+
* validation errors.
|
|
267
|
+
*
|
|
268
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
269
|
+
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
270
|
+
* to automatically add and update {@link DefaultColumns}.
|
|
257
271
|
*
|
|
258
272
|
* ### Example
|
|
259
273
|
*
|
|
260
274
|
* ```ts
|
|
261
|
-
*
|
|
275
|
+
* const result = evolu.insert("todo", {
|
|
276
|
+
* title: "Learn Evolu",
|
|
277
|
+
* isCompleted: false,
|
|
278
|
+
* });
|
|
279
|
+
*
|
|
280
|
+
* if (result.ok) {
|
|
281
|
+
* console.log("Todo created with ID:", result.value.id);
|
|
282
|
+
* } else {
|
|
283
|
+
* console.error("Validation error:", result.error);
|
|
284
|
+
* }
|
|
285
|
+
*
|
|
286
|
+
* // With onComplete callback
|
|
287
|
+
* evolu.insert(
|
|
288
|
+
* "todo",
|
|
289
|
+
* { title: "Another todo" },
|
|
290
|
+
* {
|
|
291
|
+
* onComplete: () => {
|
|
292
|
+
* console.log("Insert completed");
|
|
293
|
+
* },
|
|
294
|
+
* },
|
|
295
|
+
* );
|
|
262
296
|
* ```
|
|
263
297
|
*/
|
|
264
298
|
insert: Mutation<S, "insert">;
|
|
265
299
|
|
|
266
300
|
/**
|
|
267
|
-
* Updates a row
|
|
301
|
+
* Updates a row in the database and returns a {@link Result} with the existing
|
|
302
|
+
* {@link Id}.
|
|
303
|
+
*
|
|
304
|
+
* The first argument is the table name, and the second is an object
|
|
305
|
+
* containing the row data including the required `id` field. An optional
|
|
306
|
+
* third argument provides mutation options including an `onComplete` callback
|
|
307
|
+
* and `onlyValidate` flag.
|
|
308
|
+
*
|
|
309
|
+
* Returns a Result type - use `.ok` to check if the update succeeded, and
|
|
310
|
+
* `.value.id` to access the ID on success, or `.error` to handle validation
|
|
311
|
+
* errors.
|
|
312
|
+
*
|
|
313
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
314
|
+
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
315
|
+
* to automatically add and update {@link DefaultColumns}.
|
|
268
316
|
*
|
|
269
317
|
* ### Example
|
|
270
318
|
*
|
|
271
319
|
* ```ts
|
|
272
|
-
*
|
|
320
|
+
* const result = evolu.update("todo", {
|
|
321
|
+
* id: todoId,
|
|
322
|
+
* title: "Updated title",
|
|
323
|
+
* isCompleted: true,
|
|
324
|
+
* });
|
|
325
|
+
*
|
|
326
|
+
* if (result.ok) {
|
|
327
|
+
* console.log("Todo updated with ID:", result.value.id);
|
|
328
|
+
* } else {
|
|
329
|
+
* console.error("Validation error:", result.error);
|
|
330
|
+
* }
|
|
331
|
+
*
|
|
332
|
+
* // To delete a row, set isDeleted to true
|
|
333
|
+
* evolu.update("todo", { id: todoId, isDeleted: true });
|
|
334
|
+
*
|
|
335
|
+
* // With onComplete callback
|
|
336
|
+
* evolu.update(
|
|
337
|
+
* "todo",
|
|
338
|
+
* { id: todoId, title: "New title" },
|
|
339
|
+
* {
|
|
340
|
+
* onComplete: () => {
|
|
341
|
+
* console.log("Update completed");
|
|
342
|
+
* },
|
|
343
|
+
* },
|
|
344
|
+
* );
|
|
273
345
|
* ```
|
|
274
346
|
*/
|
|
275
347
|
update: Mutation<S, "update">;
|
|
276
348
|
|
|
277
349
|
/**
|
|
278
|
-
* Upserts a row
|
|
350
|
+
* Upserts a row in the database and returns a {@link Result} with the existing
|
|
351
|
+
* {@link Id}.
|
|
352
|
+
*
|
|
353
|
+
* The first argument is the table name, and the second is an object
|
|
354
|
+
* containing the row data including the required `id` field. An optional
|
|
355
|
+
* third argument provides mutation options including an `onComplete` callback
|
|
356
|
+
* and `onlyValidate` flag.
|
|
357
|
+
*
|
|
358
|
+
* This function allows you to use custom IDs and optionally set `createdAt`,
|
|
359
|
+
* which is useful for external systems, data migrations, or when the same row
|
|
360
|
+
* may already be created on a different device.
|
|
361
|
+
*
|
|
362
|
+
* Returns a Result type - use `.ok` to check if the upsert succeeded, and
|
|
363
|
+
* `.value.id` to access the ID on success, or `.error` to handle validation
|
|
364
|
+
* errors.
|
|
365
|
+
*
|
|
366
|
+
* Evolu does not use SQL for mutations to ensure data can be safely and
|
|
367
|
+
* predictably merged without conflicts. Explicit mutations also allow Evolu
|
|
368
|
+
* to automatically add and update {@link DefaultColumns}.
|
|
279
369
|
*
|
|
280
370
|
* ### Example
|
|
281
371
|
*
|
|
282
372
|
* ```ts
|
|
283
|
-
* //
|
|
373
|
+
* // Use deterministic ID for stable upserts across devices
|
|
374
|
+
* const stableId = createIdFromString("my-todo-1");
|
|
375
|
+
*
|
|
376
|
+
* const result = evolu.upsert("todo", {
|
|
377
|
+
* id: stableId,
|
|
378
|
+
* title: "Learn Evolu",
|
|
379
|
+
* isCompleted: false,
|
|
380
|
+
* });
|
|
381
|
+
*
|
|
382
|
+
* if (result.ok) {
|
|
383
|
+
* console.log("Todo upserted with ID:", result.value.id);
|
|
384
|
+
* } else {
|
|
385
|
+
* console.error("Validation error:", result.error);
|
|
386
|
+
* }
|
|
387
|
+
*
|
|
388
|
+
* // Data migration with custom createdAt
|
|
389
|
+
* evolu.upsert("todo", {
|
|
390
|
+
* id: externalId,
|
|
391
|
+
* title: "Migrated todo",
|
|
392
|
+
* createdAt: new Date("2023-01-01"), // Preserve original timestamp
|
|
393
|
+
* });
|
|
394
|
+
*
|
|
395
|
+
* // With onComplete callback
|
|
396
|
+
* evolu.upsert(
|
|
397
|
+
* "todo",
|
|
398
|
+
* { id: stableId, title: "Updated title" },
|
|
399
|
+
* {
|
|
400
|
+
* onComplete: () => {
|
|
401
|
+
* console.log("Upsert completed");
|
|
402
|
+
* },
|
|
403
|
+
* },
|
|
404
|
+
* );
|
|
284
405
|
* ```
|
|
285
406
|
*/
|
|
286
407
|
upsert: Mutation<S, "upsert">;
|
|
@@ -594,7 +715,11 @@ const createEvoluInstance =
|
|
|
594
715
|
const values = getMutationType(table, "insert").fromUnknown(props);
|
|
595
716
|
|
|
596
717
|
if (values.ok) {
|
|
597
|
-
const
|
|
718
|
+
const valuesWithCreatedAt = {
|
|
719
|
+
...values.value,
|
|
720
|
+
createdAt: new Date(deps.time.now()).toISOString(),
|
|
721
|
+
};
|
|
722
|
+
const dbChange = { table, id, values: valuesWithCreatedAt };
|
|
598
723
|
assertValidDbChange(dbChange);
|
|
599
724
|
initialDataDbChanges.push(dbChange);
|
|
600
725
|
return ok({ id });
|
|
@@ -646,10 +771,23 @@ const createEvoluInstance =
|
|
|
646
771
|
// We insert `undefined` to detect such a situation.
|
|
647
772
|
mutateMicrotaskQueue.push([undefined, undefined]);
|
|
648
773
|
} else {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
774
|
+
const values = { ...result.value };
|
|
775
|
+
|
|
776
|
+
delete values.id;
|
|
777
|
+
if (kind === "insert" || kind === "upsert") {
|
|
778
|
+
// Only set createdAt if not provided by user
|
|
779
|
+
if (!("createdAt" in values)) {
|
|
780
|
+
values.createdAt = new Date(deps.time.now()).toISOString();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const dbChange = {
|
|
785
|
+
table,
|
|
786
|
+
id,
|
|
787
|
+
values,
|
|
788
|
+
};
|
|
652
789
|
assertValidDbChange(dbChange);
|
|
790
|
+
|
|
653
791
|
mutateMicrotaskQueue.push([dbChange, options?.onComplete]);
|
|
654
792
|
}
|
|
655
793
|
|
package/src/Evolu/Protocol.ts
CHANGED
|
@@ -188,8 +188,10 @@ import {
|
|
|
188
188
|
} from "./Owner.js";
|
|
189
189
|
import {
|
|
190
190
|
BinaryTimestamp,
|
|
191
|
+
binaryTimestampLength,
|
|
191
192
|
binaryTimestampToTimestamp,
|
|
192
193
|
Counter,
|
|
194
|
+
eqTimestamp,
|
|
193
195
|
Millis,
|
|
194
196
|
NodeId,
|
|
195
197
|
Timestamp,
|
|
@@ -399,7 +401,8 @@ export type ProtocolError =
|
|
|
399
401
|
| ProtocolInvalidDataError
|
|
400
402
|
| ProtocolWriteKeyError
|
|
401
403
|
| ProtocolWriteError
|
|
402
|
-
| ProtocolSyncError
|
|
404
|
+
| ProtocolSyncError
|
|
405
|
+
| ProtocolTimestampMismatchError;
|
|
403
406
|
|
|
404
407
|
/** Base interface for all protocol errors. */
|
|
405
408
|
export interface ProtocolErrorBase {
|
|
@@ -445,6 +448,17 @@ export interface ProtocolSyncError extends ProtocolErrorBase {
|
|
|
445
448
|
readonly type: "ProtocolSyncError";
|
|
446
449
|
}
|
|
447
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Error when embedded timestamp doesn't match expected timestamp in
|
|
453
|
+
* EncryptedDbChange. Indicates potential tampering or corruption of CRDT
|
|
454
|
+
* messages.
|
|
455
|
+
*/
|
|
456
|
+
export interface ProtocolTimestampMismatchError {
|
|
457
|
+
readonly type: "ProtocolTimestampMismatchError";
|
|
458
|
+
readonly expected: Timestamp;
|
|
459
|
+
readonly embedded: Timestamp;
|
|
460
|
+
}
|
|
461
|
+
|
|
448
462
|
/**
|
|
449
463
|
* Creates a {@link ProtocolMessage} from CRDT messages.
|
|
450
464
|
*
|
|
@@ -469,7 +483,7 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
469
483
|
|
|
470
484
|
for (const message of messages) {
|
|
471
485
|
const change = encodeAndEncryptDbChange(deps)(
|
|
472
|
-
message
|
|
486
|
+
message,
|
|
473
487
|
owner.encryptionKey,
|
|
474
488
|
);
|
|
475
489
|
const encryptedCrdtMessage = { timestamp: message.timestamp, change };
|
|
@@ -1484,7 +1498,7 @@ const decodeTimestamps = (
|
|
|
1484
1498
|
for (let i = 0; i < length; i++) {
|
|
1485
1499
|
const deltaMillis = decodeNonNegativeInt(buffer);
|
|
1486
1500
|
const millis = Millis.from(previousMillis + deltaMillis);
|
|
1487
|
-
if (!millis.ok) throw new
|
|
1501
|
+
if (!millis.ok) throw new ProtocolDecodeError(millis.error.type);
|
|
1488
1502
|
millises.push(millis.value);
|
|
1489
1503
|
previousMillis = millis.value;
|
|
1490
1504
|
}
|
|
@@ -1493,7 +1507,7 @@ const decodeTimestamps = (
|
|
|
1493
1507
|
let counterIndex = 0;
|
|
1494
1508
|
while (counterIndex < length) {
|
|
1495
1509
|
const counter = Counter.from(decodeNonNegativeInt(buffer));
|
|
1496
|
-
if (!counter.ok) throw new
|
|
1510
|
+
if (!counter.ok) throw new ProtocolDecodeError(counter.error.type);
|
|
1497
1511
|
const runLength = decodeNonNegativeInt(buffer);
|
|
1498
1512
|
for (let i = 0; i < runLength; i++) {
|
|
1499
1513
|
counters.push(counter.value);
|
|
@@ -1659,12 +1673,24 @@ export const binaryTimestampToFingerprint = (
|
|
|
1659
1673
|
/**
|
|
1660
1674
|
* Encodes and encrypts a {@link DbChange} using the provided owner's encryption
|
|
1661
1675
|
* key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
|
|
1676
|
+
*
|
|
1677
|
+
* The format includes the protocol version for backward compatibility and the
|
|
1678
|
+
* timestamp for tamper-proof verification that the timestamp matches the change
|
|
1679
|
+
* data.
|
|
1662
1680
|
*/
|
|
1663
1681
|
export const encodeAndEncryptDbChange =
|
|
1664
1682
|
(deps: SymmetricCryptoDep) =>
|
|
1665
|
-
(
|
|
1683
|
+
(message: CrdtMessage, key: EncryptionKey): EncryptedDbChange => {
|
|
1684
|
+
const change = message.change;
|
|
1666
1685
|
const buffer = createBuffer();
|
|
1667
1686
|
|
|
1687
|
+
// Encode protocol version first for backward compatibility
|
|
1688
|
+
encodeNonNegativeInt(buffer, protocolVersion);
|
|
1689
|
+
|
|
1690
|
+
// Encode the timestamp (after version) for tamper verification
|
|
1691
|
+
const binaryTimestamp = timestampToBinaryTimestamp(message.timestamp);
|
|
1692
|
+
buffer.extend(binaryTimestamp);
|
|
1693
|
+
|
|
1668
1694
|
encodeBase64Url256(buffer, change.table);
|
|
1669
1695
|
|
|
1670
1696
|
buffer.extend(idToBinaryId(change.id));
|
|
@@ -1700,50 +1726,76 @@ export const encodeAndEncryptDbChange =
|
|
|
1700
1726
|
};
|
|
1701
1727
|
|
|
1702
1728
|
/**
|
|
1703
|
-
* Decrypts and decodes an {@link
|
|
1704
|
-
* encryption key.
|
|
1729
|
+
* Decrypts and decodes an {@link EncryptedCrdtMessage} using the provided
|
|
1730
|
+
* owner's encryption key. Verifies that the embedded timestamp matches the
|
|
1731
|
+
* expected timestamp to ensure message integrity.
|
|
1705
1732
|
*/
|
|
1706
1733
|
export const decryptAndDecodeDbChange =
|
|
1707
1734
|
(deps: SymmetricCryptoDep) =>
|
|
1708
1735
|
(
|
|
1709
|
-
|
|
1736
|
+
message: EncryptedCrdtMessage,
|
|
1710
1737
|
key: EncryptionKey,
|
|
1711
|
-
): Result<
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1738
|
+
): Result<
|
|
1739
|
+
DbChange,
|
|
1740
|
+
| SymmetricCryptoDecryptError
|
|
1741
|
+
| ProtocolInvalidDataError
|
|
1742
|
+
| ProtocolTimestampMismatchError
|
|
1743
|
+
> =>
|
|
1744
|
+
tryDecodeProtocolData<
|
|
1745
|
+
DbChange,
|
|
1746
|
+
SymmetricCryptoDecryptError | ProtocolTimestampMismatchError
|
|
1747
|
+
>(message.change, (buffer) => {
|
|
1748
|
+
const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
|
|
1749
|
+
|
|
1750
|
+
const ciphertextLength = decodeLength(buffer);
|
|
1751
|
+
const ciphertext = buffer.shiftN(ciphertextLength);
|
|
1752
|
+
|
|
1753
|
+
const plaintextBytes = deps.symmetricCrypto.decrypt(
|
|
1754
|
+
ciphertext,
|
|
1755
|
+
key,
|
|
1756
|
+
nonce,
|
|
1757
|
+
);
|
|
1758
|
+
if (!plaintextBytes.ok) return plaintextBytes;
|
|
1726
1759
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1760
|
+
buffer.reset();
|
|
1761
|
+
buffer.extend(plaintextBytes.value);
|
|
1729
1762
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1763
|
+
// Decode version (for future compatibility, no validation needed for now)
|
|
1764
|
+
decodeNonNegativeInt(buffer);
|
|
1732
1765
|
|
|
1733
|
-
|
|
1734
|
-
|
|
1766
|
+
// Decode and verify the embedded timestamp
|
|
1767
|
+
const embeddedBinaryTimestamp = buffer.shiftN(
|
|
1768
|
+
binaryTimestampLength,
|
|
1769
|
+
) as BinaryTimestamp;
|
|
1770
|
+
const embeddedTimestamp = binaryTimestampToTimestamp(
|
|
1771
|
+
embeddedBinaryTimestamp,
|
|
1772
|
+
);
|
|
1735
1773
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1774
|
+
// Verify timestamp integrity
|
|
1775
|
+
if (!eqTimestamp(embeddedTimestamp, message.timestamp)) {
|
|
1776
|
+
return err<ProtocolTimestampMismatchError>({
|
|
1777
|
+
type: "ProtocolTimestampMismatchError",
|
|
1778
|
+
expected: message.timestamp,
|
|
1779
|
+
embedded: embeddedTimestamp,
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1741
1782
|
|
|
1742
|
-
|
|
1783
|
+
const table = decodeBase64Url256WithLength(buffer);
|
|
1784
|
+
const id = decodeId(buffer);
|
|
1743
1785
|
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1786
|
+
const length = decodeLength(buffer);
|
|
1787
|
+
const values = Object.create(null) as Record<string, SqliteValue>;
|
|
1788
|
+
|
|
1789
|
+
for (let i = 0; i < length; i++) {
|
|
1790
|
+
const column = decodeBase64Url256WithLength(buffer);
|
|
1791
|
+
const value = decodeSqliteValue(buffer);
|
|
1792
|
+
values[column] = value;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
const dbChange = { table, id, values };
|
|
1796
|
+
|
|
1797
|
+
return ok(dbChange);
|
|
1798
|
+
});
|
|
1747
1799
|
|
|
1748
1800
|
/**
|
|
1749
1801
|
* Encodes a non-negative integer into a variable-length integer format. It's
|
package/src/Evolu/Schema.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
brand,
|
|
10
10
|
BrandType,
|
|
11
11
|
createTypeErrorFormatter,
|
|
12
|
+
DateIso,
|
|
12
13
|
DateIsoString,
|
|
13
14
|
IdType,
|
|
14
15
|
InferErrors,
|
|
@@ -30,7 +31,11 @@ import { Simplify } from "../Types.js";
|
|
|
30
31
|
import { DbSchema } from "./Db.js";
|
|
31
32
|
import { createIndexes, DbIndexesBuilder } from "./Kysely.js";
|
|
32
33
|
import { AppOwner, ShardOwner, SharedOwner } from "./Owner.js";
|
|
33
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
BinaryId,
|
|
36
|
+
maxProtocolMessageRangesSize,
|
|
37
|
+
CrdtMessage,
|
|
38
|
+
} from "./Protocol.js";
|
|
34
39
|
import { Query, Row } from "./Query.js";
|
|
35
40
|
import { BinaryTimestamp } from "./Timestamp.js";
|
|
36
41
|
|
|
@@ -198,6 +203,16 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
|
|
|
198
203
|
options?: SqliteQueryOptions,
|
|
199
204
|
) => Query<Simplify<R>>;
|
|
200
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Default columns automatically added to all tables.
|
|
208
|
+
*
|
|
209
|
+
* - `createdAt`: Set by Evolu when `insert` is called, or can be custom with
|
|
210
|
+
* `upsert`.
|
|
211
|
+
* - `updatedAt`: Always set by Evolu, derived from {@link CrdtMessage} timestamp.
|
|
212
|
+
* If you defer sync to avoid leaking time activity, use a custom column to
|
|
213
|
+
* preserve real update time.
|
|
214
|
+
* - `isDeleted`: Soft delete flag.
|
|
215
|
+
*/
|
|
201
216
|
export const DefaultColumns = object({
|
|
202
217
|
createdAt: DateIsoString,
|
|
203
218
|
updatedAt: DateIsoString,
|
|
@@ -346,25 +361,41 @@ export type Updateable<Props extends Record<string, AnyType>> = InferInput<
|
|
|
346
361
|
>;
|
|
347
362
|
|
|
348
363
|
/**
|
|
349
|
-
* Type Factory to create upsertable Type. It makes nullable Types optional
|
|
350
|
-
* ensures the
|
|
364
|
+
* Type Factory to create upsertable Type. It makes nullable Types optional,
|
|
365
|
+
* includes optional default columns (createdAt, isDeleted), and ensures the
|
|
366
|
+
* {@link maxMutationSize}.
|
|
351
367
|
*
|
|
352
368
|
* ### Example
|
|
353
369
|
*
|
|
354
370
|
* ```ts
|
|
355
371
|
* const UpsertableTodo = upsertable(Schema.todo);
|
|
356
372
|
* type UpsertableTodo = typeof UpsertableTodo.Type;
|
|
357
|
-
* const todo = UpsertableTodo.from({
|
|
373
|
+
* const todo = UpsertableTodo.from({
|
|
374
|
+
* id,
|
|
375
|
+
* title,
|
|
376
|
+
* createdAt: "2023-01-01T00:00:00.000Z",
|
|
377
|
+
* });
|
|
358
378
|
* if (!todo.ok) return; // handle errors
|
|
359
379
|
* ```
|
|
360
380
|
*/
|
|
361
381
|
export const upsertable = <Props extends Record<string, AnyType>>(
|
|
362
382
|
props: Props,
|
|
363
|
-
): ValidMutationSize<UpsertableProps<Props>> =>
|
|
364
|
-
|
|
383
|
+
): ValidMutationSize<UpsertableProps<Props>> => {
|
|
384
|
+
const propsWithDefaults = {
|
|
385
|
+
...props,
|
|
386
|
+
createdAt: optional(DateIso),
|
|
387
|
+
isDeleted: optional(SqliteBoolean),
|
|
388
|
+
};
|
|
389
|
+
return validMutationSize(nullableToOptional(propsWithDefaults));
|
|
390
|
+
};
|
|
365
391
|
|
|
366
392
|
export type UpsertableProps<Props extends Record<string, AnyType>> =
|
|
367
|
-
NullableToOptionalProps<
|
|
393
|
+
NullableToOptionalProps<
|
|
394
|
+
Props & {
|
|
395
|
+
createdAt: OptionalType<typeof DateIso>;
|
|
396
|
+
isDeleted: OptionalType<typeof SqliteBoolean>;
|
|
397
|
+
}
|
|
398
|
+
>;
|
|
368
399
|
|
|
369
400
|
export type Upsertable<Props extends Record<string, AnyType>> = InferInput<
|
|
370
401
|
ObjectType<UpsertableProps<Props>>
|
package/src/Evolu/Timestamp.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assert } from "../Assert.js";
|
|
2
|
+
import { createEqObject, eqNumber, eqString } from "../Eq.js";
|
|
2
3
|
import { NanoIdLibDep } from "../NanoId.js";
|
|
3
4
|
import { increment } from "../Number.js";
|
|
4
5
|
import { Order, orderUint8Array } from "../Order.js";
|
|
@@ -124,6 +125,13 @@ export const Timestamp = object({
|
|
|
124
125
|
});
|
|
125
126
|
export type Timestamp = typeof Timestamp.Type;
|
|
126
127
|
|
|
128
|
+
/** Equality function for comparing {@link Timestamp}. */
|
|
129
|
+
export const eqTimestamp = createEqObject<Timestamp>({
|
|
130
|
+
millis: eqNumber,
|
|
131
|
+
counter: eqNumber,
|
|
132
|
+
nodeId: eqString,
|
|
133
|
+
});
|
|
134
|
+
|
|
127
135
|
export const createTimestamp = ({
|
|
128
136
|
millis = minMillis,
|
|
129
137
|
counter = minCounter,
|
package/src/Type.ts
CHANGED
|
@@ -1994,7 +1994,11 @@ export const TrimString = trim(String);
|
|
|
1994
1994
|
*
|
|
1995
1995
|
* ### Example
|
|
1996
1996
|
*
|
|
1997
|
-
*
|
|
1997
|
+
* ```ts
|
|
1998
|
+
* DateIso.from(new Date("2023-12-25T10:30:00.000Z")); // ok("2023-12-25T10:30:00.000Z")
|
|
1999
|
+
* DateIso.to("2023-12-25T10:30:00.000Z"); // Date object
|
|
2000
|
+
* DateIso.from(new Date("invalid")); // err({ type: "DateIsoString", value: "Invalid Date" })
|
|
2001
|
+
* ```
|
|
1998
2002
|
*
|
|
1999
2003
|
* @category String
|
|
2000
2004
|
*/
|