@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.
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +1 -1
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +48 -37
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +27 -50
- package/dist/src/Evolu/Db.d.ts +138 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +248 -645
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +79 -116
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +275 -132
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +129 -83
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +80 -89
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +114 -191
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +409 -416
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Relay.d.ts +1 -2
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +11 -9
- package/dist/src/Evolu/Schema.d.ts +88 -27
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +141 -24
- package/dist/src/Evolu/Storage.d.ts +158 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +32 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +29 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +20 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +144 -22
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +5 -2
- package/dist/src/Sqlite.d.ts +20 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +431 -341
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +458 -466
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +2 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/package.json +7 -7
- package/src/Assert.ts +2 -4
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +78 -91
- package/src/Evolu/Db.ts +455 -947
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +545 -307
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +191 -131
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +536 -653
- package/src/Evolu/Public.ts +7 -9
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Relay.ts +17 -12
- package/src/Evolu/Schema.ts +271 -66
- package/src/Evolu/Storage.ts +263 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +30 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +149 -23
- package/src/Sqlite.ts +59 -24
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +657 -695
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +2 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -82
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -97
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Public.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
27
|
+
timestampBytesToTimestamp,
|
|
28
|
+
timestampToTimestampBytes,
|
|
30
29
|
} from "./Timestamp.js";
|
|
31
30
|
export type {
|
|
32
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
175
|
+
// Kysely expects strict AST.
|
|
176
176
|
// prettier-ignore
|
|
177
177
|
export function jsonBuildObject<O extends Record<string, Expression<unknown>>>(
|
|
178
178
|
obj: O,
|
package/src/Evolu/Relay.ts
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
|
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:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
130
|
+
${timestampToTimestampBytes(message.timestamp)},
|
|
126
131
|
${message.change}
|
|
127
132
|
)
|
|
128
133
|
on conflict do nothing;
|
package/src/Evolu/Schema.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
6
|
-
import {
|
|
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
|
-
|
|
10
|
-
BrandType,
|
|
11
|
-
createTypeErrorFormatter,
|
|
16
|
+
array,
|
|
12
17
|
DateIso,
|
|
13
|
-
|
|
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
|
-
|
|
35
|
+
ValidMutationSize,
|
|
36
|
+
validMutationSize,
|
|
37
|
+
ValidMutationSizeError,
|
|
29
38
|
} from "../Type.js";
|
|
30
39
|
import { Simplify } from "../Types.js";
|
|
31
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
201
|
+
readonly timestamp: TimestampBytes;
|
|
192
202
|
readonly table: keyof S;
|
|
193
|
-
readonly id:
|
|
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:
|
|
217
|
-
updatedAt:
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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);
|