@evolu/common 6.0.1-preview.2 → 6.0.1-preview.4
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 +21 -13
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +79 -63
- package/dist/src/Evolu/Evolu.d.ts +2 -2
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +19 -24
- package/dist/src/Evolu/Protocol.d.ts +12 -12
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +16 -6
- package/dist/src/Evolu/Schema.d.ts +24 -40
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +13 -72
- package/package.json +2 -2
- package/src/Evolu/Db.ts +90 -76
- package/src/Evolu/Evolu.ts +34 -31
- package/src/Evolu/Protocol.ts +16 -13
- package/src/Evolu/Schema.ts +102 -130
|
@@ -130,7 +130,8 @@ import { eqArrayNumber } from "../Eq.js";
|
|
|
130
130
|
import { computeBalancedBuckets } from "../Number.js";
|
|
131
131
|
import { objectToEntries } from "../Object.js";
|
|
132
132
|
import { err, ok } from "../Result.js";
|
|
133
|
-
import {
|
|
133
|
+
import { SqliteValue } from "../Sqlite.js";
|
|
134
|
+
import { Base64Url, DateIsoString, Id, idTypeValueLength, JsonValueFromString, maxLength, NonNegativeInt, Number, object, record, } from "../Type.js";
|
|
134
135
|
import { writeKeyLength, } from "./Owner.js";
|
|
135
136
|
import { binaryTimestampToTimestamp, Counter, Millis, timestampToBinaryTimestamp, } from "./Timestamp.js";
|
|
136
137
|
/** Maximum size of the entire protocol message in bytes. */
|
|
@@ -148,6 +149,20 @@ export const ProtocolErrorCode = {
|
|
|
148
149
|
/** A code for {@link ProtocolSyncError}. */
|
|
149
150
|
SyncError: 3,
|
|
150
151
|
};
|
|
152
|
+
/**
|
|
153
|
+
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
154
|
+
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
155
|
+
*/
|
|
156
|
+
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
157
|
+
/**
|
|
158
|
+
* A DbChange is a change to a table row. Together with a unique
|
|
159
|
+
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
160
|
+
*/
|
|
161
|
+
export const DbChange = object({
|
|
162
|
+
table: Base64Url256,
|
|
163
|
+
id: Id,
|
|
164
|
+
values: record(Base64Url256, SqliteValue),
|
|
165
|
+
});
|
|
151
166
|
export const RangeType = {
|
|
152
167
|
Fingerprint: 1,
|
|
153
168
|
Skip: 0,
|
|
@@ -840,11 +855,6 @@ export const idToBinaryId = (id) => base64Url256ToBytes(id);
|
|
|
840
855
|
export const binaryIdToId = (binaryId) => decodeId(createBuffer(binaryId));
|
|
841
856
|
export const ownerIdToBinaryOwnerId = (ownerId) => base64Url256ToBytes(ownerId);
|
|
842
857
|
export const binaryOwnerIdToOwnerId = (binaryOwnerId) => decodeOwnerId(createBuffer(binaryOwnerId));
|
|
843
|
-
/**
|
|
844
|
-
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
845
|
-
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
846
|
-
*/
|
|
847
|
-
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
848
858
|
/**
|
|
849
859
|
* Alphabet used for Base64Url encoding. This is copied from the `nanoid`
|
|
850
860
|
* library to avoid dependency on a specific version of `nanoid`.
|
|
@@ -2,7 +2,7 @@ import { Kysely, SelectQueryBuilder } from "kysely";
|
|
|
2
2
|
import { ReadonlyRecord } from "../Object.js";
|
|
3
3
|
import { Result } from "../Result.js";
|
|
4
4
|
import { SqliteBoolean, SqliteQueryOptions, SqliteValue } from "../Sqlite.js";
|
|
5
|
-
import { AnyType, BrandType, InferErrors, InferInput, InferType, MergeObjectTypeErrors, NullableToOptionalProps, ObjectType, OptionalType, Type, TypeError } from "../Type.js";
|
|
5
|
+
import { AnyType, BrandType, IdType, InferErrors, InferInput, InferType, MergeObjectTypeErrors, NullableToOptionalProps, ObjectType, OptionalType, Type, TypeError } from "../Type.js";
|
|
6
6
|
import { Simplify } from "../Types.js";
|
|
7
7
|
import { DbSchema } from "./Db.js";
|
|
8
8
|
import { DbIndexesBuilder } from "./Kysely.js";
|
|
@@ -13,12 +13,6 @@ import { BinaryTimestamp } from "./Timestamp.js";
|
|
|
13
13
|
/**
|
|
14
14
|
* Defines the schema of an Evolu database.
|
|
15
15
|
*
|
|
16
|
-
* - Each top-level key represents a table name.
|
|
17
|
-
* - The value for each table name is a record of column names mapped to their
|
|
18
|
-
* respective data types, defined by {@link Type}.
|
|
19
|
-
* - Each table must include a mandatory `id` column of type {@link Id}.
|
|
20
|
-
* - No table may contain {@link DefaultColumns}.
|
|
21
|
-
*
|
|
22
16
|
* Table schema defines columns that are required for table rows. For not
|
|
23
17
|
* required columns, use {@link nullOr}.
|
|
24
18
|
*
|
|
@@ -50,9 +44,28 @@ import { BinaryTimestamp } from "./Timestamp.js";
|
|
|
50
44
|
* };
|
|
51
45
|
* ```
|
|
52
46
|
*/
|
|
53
|
-
export type EvoluSchema = ReadonlyRecord<string, ReadonlyRecord<string, Type<any, any, any, any, any
|
|
54
|
-
|
|
55
|
-
}
|
|
47
|
+
export type EvoluSchema = ReadonlyRecord<string, ReadonlyRecord<string, Type<any, any, any, any, any>>>;
|
|
48
|
+
/**
|
|
49
|
+
* Validates an {@link EvoluSchema} at compile time, returning the first error
|
|
50
|
+
* found as a readable string literal type. This approach provides much clearer
|
|
51
|
+
* and more actionable TypeScript errors than the default, which are often hard
|
|
52
|
+
* to read.
|
|
53
|
+
*
|
|
54
|
+
* Validates the following schema requirements:
|
|
55
|
+
*
|
|
56
|
+
* 1. All tables must have an 'id' column
|
|
57
|
+
* 2. The 'id' column must be a branded ID type (created with id() function)
|
|
58
|
+
* 3. Tables cannot use default column names (createdAt, updatedAt, isDeleted)
|
|
59
|
+
* 4. All column types must be compatible with SQLite (extend SqliteValue)
|
|
60
|
+
*/
|
|
61
|
+
export type ValidateSchema<S extends EvoluSchema> = ValidateSchemaHasId<S> extends never ? ValidateIdColumnType<S> extends never ? ValidateNoDefaultColumns<S> extends never ? ValidateColumnTypes<S> extends never ? S : ValidateColumnTypes<S> : ValidateNoDefaultColumns<S> : ValidateIdColumnType<S> : ValidateSchemaHasId<S>;
|
|
62
|
+
export type ValidateSchemaHasId<S extends EvoluSchema> = keyof S extends infer TableName ? TableName extends keyof S ? "id" extends keyof S[TableName] ? never : SchemaValidationError<`Table "${TableName & string}" is missing required id column.`> : never : never;
|
|
63
|
+
export type ValidateIdColumnType<S extends EvoluSchema> = keyof S extends infer TableName ? TableName extends keyof S ? "id" extends keyof S[TableName] ? S[TableName]["id"] extends IdType<any> ? never : SchemaValidationError<`Table "${TableName & string}" id column must be a branded ID type (created with id("${TableName & string}")).`> : never : never : never;
|
|
64
|
+
export type ValidateNoDefaultColumns<S extends EvoluSchema> = keyof S extends infer TableName ? TableName extends keyof S ? keyof S[TableName] extends infer ColumnName ? ColumnName extends keyof S[TableName] ? ColumnName extends "createdAt" | "updatedAt" | "isDeleted" ? SchemaValidationError<`Table "${TableName & string}" uses default column name "${ColumnName & string}". Default columns (createdAt, updatedAt, isDeleted) are added automatically.`> : never : never : never : never : never;
|
|
65
|
+
export type ValidateColumnTypes<S extends EvoluSchema> = keyof S extends infer TableName ? TableName extends keyof S ? keyof S[TableName] extends infer ColumnName ? ColumnName extends keyof S[TableName] ? InferType<S[TableName][ColumnName]> extends SqliteValue ? never : SchemaValidationError<`Table "${TableName & string}" column "${ColumnName & string}" type is not compatible with SQLite. Column types must extend SqliteValue (string, number, Uint8Array, or null).`> : never : never : never : never;
|
|
66
|
+
/** Schema validation error that shows clear, readable messages */
|
|
67
|
+
export type SchemaValidationError<Message extends string> = `❌ Schema Error: ${Message}`;
|
|
68
|
+
export declare const evoluSchemaToDbSchema: (schema: EvoluSchema, indexes?: DbIndexesBuilder) => DbSchema;
|
|
56
69
|
export type CreateQuery<S extends EvoluSchema> = <R extends Row>(queryCallback: (db: Pick<Kysely<{
|
|
57
70
|
[Table in keyof S]: {
|
|
58
71
|
readonly [Column in keyof S[Table]]: Column extends "id" | "createdAt" | "updatedAt" ? InferType<S[Table][Column]> : InferType<S[Table][Column]> | null;
|
|
@@ -61,7 +74,7 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(queryCallback:
|
|
|
61
74
|
readonly evolu_history: {
|
|
62
75
|
readonly timestamp: BinaryTimestamp;
|
|
63
76
|
readonly table: keyof S;
|
|
64
|
-
readonly
|
|
77
|
+
readonly id: BinaryId;
|
|
65
78
|
readonly column: string;
|
|
66
79
|
readonly value: SqliteValue;
|
|
67
80
|
};
|
|
@@ -72,35 +85,6 @@ export declare const DefaultColumns: ObjectType<{
|
|
|
72
85
|
isDeleted: import("../Type.js").UnionType<[Type<"Null", null, null, import("../Type.js").NullError, null, import("../Type.js").NullError>, import("../Type.js").TransformType<Type<"Boolean", boolean, boolean, import("../Type.js").BooleanError, boolean, import("../Type.js").BooleanError>, import("../Type.js").UnionType<[import("../Type.js").LiteralType<0>, import("../Type.js").LiteralType<1>]>, never>]>;
|
|
73
86
|
}>;
|
|
74
87
|
export type DefaultColumns = typeof DefaultColumns.Type;
|
|
75
|
-
/**
|
|
76
|
-
* Valid {@link EvoluSchema}.
|
|
77
|
-
*
|
|
78
|
-
* - Table and column names must be Base64Url strings.
|
|
79
|
-
* - Each table must include an `id` column of type {@link Id}.
|
|
80
|
-
* - Default column names (`createdAt`, `updatedAt`, `isDeleted`) are not allowed.
|
|
81
|
-
*/
|
|
82
|
-
export declare const ValidEvoluSchema: BrandType<import("../Type.js").RecordType<"Brand", string & import("../Types.js").Brand<"Base64Url"> & import("../Types.js").Brand<"MaxLength256">, string, import("../Type.js").MaxLengthError<256>, string & import("../Types.js").Brand<"Base64Url">, import("../Type.js").StringError | import("../Type.js").RegexError<"Base64Url">, import("../Type.js").ObjectWithRecordType<{
|
|
83
|
-
id: Type<"EvoluType", AnyType, AnyType, import("../Type.js").EvoluTypeError, AnyType, import("../Type.js").EvoluTypeError>;
|
|
84
|
-
}, "Brand", string & import("../Types.js").Brand<"Base64Url"> & import("../Types.js").Brand<"MaxLength256">, string, import("../Type.js").MaxLengthError<256>, string & import("../Types.js").Brand<"Base64Url">, import("../Type.js").StringError | import("../Type.js").RegexError<"Base64Url">, Type<"Unknown", unknown, unknown, never, unknown, never>>>, "ValidEvoluSchema", ValidEvoluSchemaError, import("../Type.js").RecordError<import("../Type.js").MaxLengthError<256>, import("../Type.js").ObjectWithRecordError<{
|
|
85
|
-
id: import("../Type.js").EvoluTypeError;
|
|
86
|
-
}, import("../Type.js").MaxLengthError<256>, never>> | import("../Type.js").RecordError<import("../Type.js").StringError | import("../Type.js").RegexError<"Base64Url">, import("../Type.js").ObjectWithRecordError<{
|
|
87
|
-
id: import("../Type.js").EvoluTypeError;
|
|
88
|
-
}, import("../Type.js").StringError | import("../Type.js").RegexError<"Base64Url">, never>>>;
|
|
89
|
-
export type ValidEvoluSchema = typeof ValidEvoluSchema.Type;
|
|
90
|
-
export interface ValidEvoluSchemaError extends TypeError<"ValidEvoluSchema"> {
|
|
91
|
-
readonly reason: {
|
|
92
|
-
kind: "DefaultColumnError";
|
|
93
|
-
tableName: string;
|
|
94
|
-
columnName: string;
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Asserts that the given value is {@link ValidEvoluSchema}.
|
|
99
|
-
*
|
|
100
|
-
* Throws an error if the value is not a valid Evolu schema.
|
|
101
|
-
*/
|
|
102
|
-
export declare const assertValidEvoluSchema: (value: unknown) => ValidEvoluSchema;
|
|
103
|
-
export declare const validEvoluSchemaToDbSchema: (validEvoluSchema: ValidEvoluSchema, indexes?: DbIndexesBuilder) => DbSchema;
|
|
104
88
|
export type MutationKind = "insert" | "update" | "upsert";
|
|
105
89
|
export type Mutation<S extends EvoluSchema, Kind extends MutationKind> = <TableName extends keyof S>(table: TableName, props: InferInput<ObjectType<MutationMapping<S[TableName], Kind>>>, options?: MutationOptions) => Result<{
|
|
106
90
|
readonly id: S[TableName]["id"]["Type"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Schema.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"Schema.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAGpD,OAAO,EAA8B,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAW,MAAM,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EACL,OAAO,EAEP,SAAS,EAGT,MAAM,EACN,WAAW,EACX,UAAU,EACV,SAAS,EACT,qBAAqB,EAErB,uBAAuB,EAGvB,UAAU,EAGV,YAAY,EACZ,IAAI,EACJ,SAAS,EACV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAiB,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAY,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAgC,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,MAAM,WAAW,GAAG,cAAc,CACtC,MAAM,EAEN,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CACtD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,WAAW,IAC9C,mBAAmB,CAAC,CAAC,CAAC,SAAS,KAAK,GAChC,oBAAoB,CAAC,CAAC,CAAC,SAAS,KAAK,GACnC,wBAAwB,CAAC,CAAC,CAAC,SAAS,KAAK,GACvC,mBAAmB,CAAC,CAAC,CAAC,SAAS,KAAK,GAClC,CAAC,GACD,mBAAmB,CAAC,CAAC,CAAC,GACxB,wBAAwB,CAAC,CAAC,CAAC,GAC7B,oBAAoB,CAAC,CAAC,CAAC,GACzB,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAE7B,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,WAAW,IACnD,MAAM,CAAC,SAAS,MAAM,SAAS,GAC3B,SAAS,SAAS,MAAM,CAAC,GACvB,IAAI,SAAS,MAAM,CAAC,CAAC,SAAS,CAAC,GAC7B,KAAK,GACL,qBAAqB,CAAC,UAAU,SAAS,GAAG,MAAM,kCAAkC,CAAC,GACvF,KAAK,GACP,KAAK,CAAC;AAEZ,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,WAAW,IACpD,MAAM,CAAC,SAAS,MAAM,SAAS,GAC3B,SAAS,SAAS,MAAM,CAAC,GACvB,IAAI,SAAS,MAAM,CAAC,CAAC,SAAS,CAAC,GAC7B,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,GAAG,CAAC,GACpC,KAAK,GACL,qBAAqB,CAAC,UAAU,SAAS,GAAG,MAAM,2DAA2D,SAAS,GAAG,MAAM,MAAM,CAAC,GACxI,KAAK,GACP,KAAK,GACP,KAAK,CAAC;AAEZ,MAAM,MAAM,wBAAwB,CAAC,CAAC,SAAS,WAAW,IACxD,MAAM,CAAC,SAAS,MAAM,SAAS,GAC3B,SAAS,SAAS,MAAM,CAAC,GACvB,MAAM,CAAC,CAAC,SAAS,CAAC,SAAS,MAAM,UAAU,GACzC,UAAU,SAAS,MAAM,CAAC,CAAC,SAAS,CAAC,GACnC,UAAU,SAAS,WAAW,GAAG,WAAW,GAAG,WAAW,GACxD,qBAAqB,CAAC,UAAU,SAAS,GAAG,MAAM,+BAA+B,UAAU,GAAG,MAAM,+EAA+E,CAAC,GACpL,KAAK,GACP,KAAK,GACP,KAAK,GACP,KAAK,GACP,KAAK,CAAC;AAEZ,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,WAAW,IACnD,MAAM,CAAC,SAAS,MAAM,SAAS,GAC3B,SAAS,SAAS,MAAM,CAAC,GACvB,MAAM,CAAC,CAAC,SAAS,CAAC,SAAS,MAAM,UAAU,GACzC,UAAU,SAAS,MAAM,CAAC,CAAC,SAAS,CAAC,GACnC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAC,SAAS,WAAW,GACrD,KAAK,GACL,qBAAqB,CAAC,UAAU,SAAS,GAAG,MAAM,aAAa,UAAU,GAAG,MAAM,mHAAmH,CAAC,GACxM,KAAK,GACP,KAAK,GACP,KAAK,GACP,KAAK,CAAC;AAEZ,kEAAkE;AAClE,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,IACtD,mBAAmB,OAAO,EAAE,CAAC;AAE/B,eAAO,MAAM,qBAAqB,GAChC,QAAQ,WAAW,EACnB,UAAU,gBAAgB,KACzB,QAgBF,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,CAAC,SAAS,GAAG,EAC7D,aAAa,EAAE,CACb,EAAE,EAAE,IAAI,CACN,MAAM,CACJ;KACG,KAAK,IAAI,MAAM,CAAC,GAAG;QAClB,QAAQ,EAAE,MAAM,IAAI,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,MAAM,SACvC,IAAI,GACJ,WAAW,GACX,WAAW,GACX,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAC3B,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI;KACvC,GAAG,cAAc;CACnB,GAAG;IACF,QAAQ,CAAC,aAAa,EAAE;QACtB,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;QACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACxB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;KAC7B,CAAC;CACH,CACF,EACD,YAAY,GAAG,IAAI,GAAG,MAAM,GAAG,eAAe,CAC/C,KACE,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,EACpC,OAAO,CAAC,EAAE,kBAAkB,KACzB,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AAExB,eAAO,MAAM,cAAc;;;;EAIzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAC;AAExD,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1D,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,WAAW,EAAE,IAAI,SAAS,YAAY,IAAI,CACvE,SAAS,SAAS,MAAM,CAAC,EAEzB,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAClE,OAAO,CAAC,EAAE,eAAe,KACtB,MAAM,CACT;IAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;CAAE,EACzC,sBAAsB,GACtB,qBAAqB,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CACzE,CAAC;AAEF,MAAM,MAAM,eAAe,CACzB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,YAAY,IACpB,CAAC,SAAS,QAAQ,GAClB,eAAe,CAAC,CAAC,CAAC,GAClB,CAAC,SAAS,QAAQ,GAChB,eAAe,CAAC,CAAC,CAAC,GAClB,eAAe,CAAC,CAAC,CAAC,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAEhC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,WAAW,CAAC;CAC3C;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,SAAS,CAAC;AAStC,MAAM,WAAW,sBACf,SAAQ,SAAS,CAAC,mBAAmB,CAAC;CAAG;AAE3C,eAAO,MAAM,4BAA4B,iEAItC,CAAC;AAEJ,MAAM,MAAM,iBAAiB,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACjE,SAAS,CACP,UAAU,CAAC,KAAK,CAAC,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAC/B,CAAC;AAEJ;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9D,OAAO,KAAK,KACX,iBAAiB,CAAC,eAAe,CAAC,KAAK,CAAC,CAI1C,CAAC;AAEF,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CACvE,uBAAuB,CAAC,KAAK,CAAC,EAC9B,IAAI,CACL,CAAC;AAEF,MAAM,MAAM,UAAU,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,UAAU,CACxE,UAAU,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CACnC,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9D,OAAO,KAAK,KACX,iBAAiB,CAAC,eAAe,CAAC,KAAK,CAAC,CAM1C,CAAC;AAEF,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;KAClE,CAAC,IAAI,MAAM,KAAK,GAAG,CAAC,SAAS,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;CACvE,GAAG;IAAE,SAAS,EAAE,YAAY,CAAC,OAAO,aAAa,CAAC,CAAA;CAAE,CAAC;AAEtD,MAAM,MAAM,UAAU,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,UAAU,CACxE,UAAU,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CACnC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9D,OAAO,KAAK,KACX,iBAAiB,CAAC,eAAe,CAAC,KAAK,CAAC,CACG,CAAC;AAE/C,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAC/D,uBAAuB,CAAC,KAAK,CAAC,CAAC;AAEjC,MAAM,MAAM,UAAU,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,UAAU,CACxE,UAAU,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CACnC,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,WAAW,IAAI;KACxD,KAAK,IAAI,MAAM,CAAC,GAAG,uBAAuB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;CACtD,CAAC,MAAM,CAAC,CAAC,CAAC;AAEX,MAAM,MAAM,uBAAuB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACjE,iBAAiB,CAAC,CAAC,EAAE,QAAQ,CAAC,GAC9B,iBAAiB,CAAC,CAAC,EAAE,QAAQ,CAAC,GAC9B,iBAAiB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAEnC,MAAM,MAAM,iBAAiB,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,YAAY,IACpB;KACD,MAAM,IAAI,MAAM,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,WAAW,CAClD,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAC9B;CACF,CAAC,MAAM,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC"}
|
package/dist/src/Evolu/Schema.js
CHANGED
|
@@ -1,86 +1,27 @@
|
|
|
1
1
|
import { pack } from "msgpackr";
|
|
2
|
+
import { assert } from "../Assert.js";
|
|
2
3
|
import { mapObject, objectToEntries } from "../Object.js";
|
|
3
4
|
import { err, ok } from "../Result.js";
|
|
4
5
|
import { SqliteBoolean } from "../Sqlite.js";
|
|
5
|
-
import { brand, createTypeErrorFormatter, DateIsoString,
|
|
6
|
+
import { brand, createTypeErrorFormatter, DateIsoString, nullableToOptional, nullOr, object, omit, optional, } from "../Type.js";
|
|
7
|
+
import { DbSchema } from "./Db.js";
|
|
6
8
|
import { createIndexes } from "./Kysely.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
createdAt: DateIsoString,
|
|
10
|
-
updatedAt: DateIsoString,
|
|
11
|
-
isDeleted: nullOr(SqliteBoolean),
|
|
12
|
-
});
|
|
13
|
-
const isDefaultColumnName = (value) => value === "createdAt" || value === "updatedAt" || value === "isDeleted";
|
|
14
|
-
/**
|
|
15
|
-
* Valid {@link EvoluSchema}.
|
|
16
|
-
*
|
|
17
|
-
* - Table and column names must be Base64Url strings.
|
|
18
|
-
* - Each table must include an `id` column of type {@link Id}.
|
|
19
|
-
* - Default column names (`createdAt`, `updatedAt`, `isDeleted`) are not allowed.
|
|
20
|
-
*/
|
|
21
|
-
export const ValidEvoluSchema = brand("ValidEvoluSchema", record(Base64Url256, object({ id: EvoluType }, record(Base64Url256, Unknown))), (value) => {
|
|
22
|
-
for (const tableName in value) {
|
|
23
|
-
for (const columnName in value[tableName]) {
|
|
24
|
-
if (isDefaultColumnName(columnName)) {
|
|
25
|
-
return err({
|
|
26
|
-
type: "ValidEvoluSchema",
|
|
27
|
-
value,
|
|
28
|
-
reason: {
|
|
29
|
-
kind: "DefaultColumnError",
|
|
30
|
-
tableName,
|
|
31
|
-
columnName,
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return ok(value);
|
|
38
|
-
});
|
|
39
|
-
/**
|
|
40
|
-
* Asserts that the given value is {@link ValidEvoluSchema}.
|
|
41
|
-
*
|
|
42
|
-
* Throws an error if the value is not a valid Evolu schema.
|
|
43
|
-
*/
|
|
44
|
-
export const assertValidEvoluSchema = (value) => {
|
|
45
|
-
const validEvoluSchema = ValidEvoluSchema.fromUnknown(value);
|
|
46
|
-
if (!validEvoluSchema.ok) {
|
|
47
|
-
const message = formatValidEvoluSchemaError(validEvoluSchema.error);
|
|
48
|
-
throw new Error(`Invalid Evolu schema: ${message}`);
|
|
49
|
-
}
|
|
50
|
-
return validEvoluSchema.value;
|
|
51
|
-
};
|
|
52
|
-
const formatValidEvoluSchemaError = (error) => {
|
|
53
|
-
if (error.type === "Record") {
|
|
54
|
-
if (error.reason.kind === "Key") {
|
|
55
|
-
return `The table "${error.reason.key}" has invalid name. A table name must be Base64Url256 string (A-Z, a-z, 0-9, -, _).`;
|
|
56
|
-
}
|
|
57
|
-
if (error.reason.kind === "Value" &&
|
|
58
|
-
error.reason.error.reason.kind === "Props" &&
|
|
59
|
-
error.reason.error.reason.errors.id?.type === "EvoluType") {
|
|
60
|
-
return `The table "${error.reason.key}" has invalid ID column. Check examples.`;
|
|
61
|
-
}
|
|
62
|
-
if (error.reason.kind === "Value" &&
|
|
63
|
-
error.reason.error.reason.kind === "IndexKey") {
|
|
64
|
-
return `The table "${error.reason.key}" has invalid column name "${error.reason.error.reason.key}". A column name must be Base64Url256 string (A-Z, a-z, 0-9, -, _).`;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (error.type === "ValidEvoluSchema") {
|
|
68
|
-
return `The table "${error.reason.tableName}" uses reserved column name "${error.reason.columnName}". Reserved column names are: createdAt, updatedAt, isDeleted.`;
|
|
69
|
-
}
|
|
70
|
-
return JSON.stringify(error, null, 2);
|
|
71
|
-
};
|
|
72
|
-
export const validEvoluSchemaToDbSchema = (validEvoluSchema, indexes) => {
|
|
73
|
-
const tables = objectToEntries(validEvoluSchema).map(([tableName, table]) => ({
|
|
9
|
+
export const evoluSchemaToDbSchema = (schema, indexes) => {
|
|
10
|
+
const tables = objectToEntries(schema).map(([tableName, table]) => ({
|
|
74
11
|
name: tableName,
|
|
75
12
|
columns: objectToEntries(table)
|
|
76
13
|
.filter(([k]) => k !== "id")
|
|
77
14
|
.map(([k]) => k),
|
|
78
15
|
}));
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
16
|
+
const dbSchema = { tables, indexes: createIndexes(indexes) };
|
|
17
|
+
assert(DbSchema.is(dbSchema), "Invalid EvoluSchema: Table and column names must use only characters A-Za-z0-9_- and be at most 256 characters long.");
|
|
18
|
+
return dbSchema;
|
|
83
19
|
};
|
|
20
|
+
export const DefaultColumns = object({
|
|
21
|
+
createdAt: DateIsoString,
|
|
22
|
+
updatedAt: DateIsoString,
|
|
23
|
+
isDeleted: nullOr(SqliteBoolean),
|
|
24
|
+
});
|
|
84
25
|
/**
|
|
85
26
|
* Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
|
|
86
27
|
* the {@link maxProtocolMessageRangesSize}. The max size is 640KB in bytes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evolu/common",
|
|
3
|
-
"version": "6.0.1-preview.
|
|
3
|
+
"version": "6.0.1-preview.4",
|
|
4
4
|
"description": "TypeScript library and local-first framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"evolu",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"better-sqlite3": "^11.10.0",
|
|
58
58
|
"shx": "^0.3.4",
|
|
59
59
|
"typescript": "^5.8.3",
|
|
60
|
-
"vitest": "^3.2.
|
|
60
|
+
"vitest": "^3.2.3",
|
|
61
61
|
"ws": "^8.18.2",
|
|
62
62
|
"@evolu/tsconfig": "0.0.2"
|
|
63
63
|
},
|
package/src/Evolu/Db.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
SqliteValue,
|
|
35
35
|
} from "../Sqlite.js";
|
|
36
36
|
import { TimeDep } from "../Time.js";
|
|
37
|
-
import { Id, Mnemonic, object, SimpleName, String } from "../Type.js";
|
|
37
|
+
import { array, Id, Mnemonic, object, SimpleName, String } from "../Type.js";
|
|
38
38
|
import {
|
|
39
39
|
createInitializedWorker,
|
|
40
40
|
Worker,
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
Base64Url256,
|
|
55
55
|
BinaryId,
|
|
56
56
|
binaryIdToId,
|
|
57
|
+
BinaryOwnerId,
|
|
57
58
|
CrdtMessage,
|
|
58
59
|
createProtocolMessageForSync,
|
|
59
60
|
createProtocolMessageFromCrdtMessages,
|
|
@@ -96,19 +97,21 @@ import {
|
|
|
96
97
|
timestampToTimestampString,
|
|
97
98
|
} from "./Timestamp.js";
|
|
98
99
|
|
|
99
|
-
export
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface DbTable {
|
|
105
|
-
readonly name: Base64Url256;
|
|
106
|
-
readonly columns: ReadonlyArray<Base64Url256>;
|
|
107
|
-
}
|
|
100
|
+
export const DbTable = object({
|
|
101
|
+
name: Base64Url256,
|
|
102
|
+
columns: array(Base64Url256),
|
|
103
|
+
});
|
|
104
|
+
export type DbTable = typeof DbTable.Type;
|
|
108
105
|
|
|
109
106
|
export const DbIndex = object({ name: String, sql: String });
|
|
110
107
|
export type DbIndex = typeof DbIndex.Type;
|
|
111
108
|
|
|
109
|
+
export const DbSchema = object({
|
|
110
|
+
tables: array(DbTable),
|
|
111
|
+
indexes: array(DbIndex),
|
|
112
|
+
});
|
|
113
|
+
export type DbSchema = typeof DbSchema.Type;
|
|
114
|
+
|
|
112
115
|
export type DbWorker = Worker<DbWorkerInput, DbWorkerOutput>;
|
|
113
116
|
|
|
114
117
|
export type CreateDbWorker = (name: SimpleName) => DbWorker;
|
|
@@ -496,7 +499,6 @@ export const createDbWorkerForPlatform = (
|
|
|
496
499
|
onCompleteId: message.onCompleteId,
|
|
497
500
|
reload: message.reload,
|
|
498
501
|
});
|
|
499
|
-
deps.sqlite[Symbol.dispose]();
|
|
500
502
|
|
|
501
503
|
break;
|
|
502
504
|
}
|
|
@@ -621,34 +623,32 @@ const indexesAreEqual = (self: DbIndex, that: DbIndex): boolean =>
|
|
|
621
623
|
|
|
622
624
|
export interface DbSnapshot {
|
|
623
625
|
readonly schema: DbSchema;
|
|
624
|
-
readonly
|
|
625
|
-
rows: ReadonlyArray<SqliteRow>;
|
|
626
|
+
readonly tables: Array<{
|
|
626
627
|
name: string;
|
|
628
|
+
rows: ReadonlyArray<SqliteRow>;
|
|
627
629
|
}>;
|
|
628
630
|
}
|
|
629
631
|
|
|
630
|
-
// TODO: Move
|
|
631
|
-
export const getDbSnapshot = (
|
|
632
|
-
deps: SqliteDep,
|
|
633
|
-
): Result<DbSnapshot, SqliteError> => {
|
|
632
|
+
// TODO: Move to test helpers.
|
|
633
|
+
export const getDbSnapshot = (deps: SqliteDep): DbSnapshot => {
|
|
634
634
|
const schema = getDbSchema(deps)({ allIndexes: true });
|
|
635
|
-
|
|
635
|
+
assert(schema.ok, "bug");
|
|
636
636
|
|
|
637
|
-
const
|
|
637
|
+
const tables = [];
|
|
638
638
|
|
|
639
639
|
for (const table of schema.value.tables) {
|
|
640
640
|
const result = deps.sqlite.exec(sql`
|
|
641
641
|
select * from ${sql.identifier(table.name)};
|
|
642
642
|
`);
|
|
643
|
-
|
|
643
|
+
assert(result.ok, "bug");
|
|
644
644
|
|
|
645
|
-
|
|
646
|
-
rows: result.value.rows,
|
|
645
|
+
tables.push({
|
|
647
646
|
name: table.name,
|
|
647
|
+
rows: result.value.rows,
|
|
648
648
|
});
|
|
649
649
|
}
|
|
650
650
|
|
|
651
|
-
return
|
|
651
|
+
return { schema: schema.value, tables };
|
|
652
652
|
};
|
|
653
653
|
|
|
654
654
|
const ensureDbSchema =
|
|
@@ -787,41 +787,39 @@ const initializeDb =
|
|
|
787
787
|
`,
|
|
788
788
|
|
|
789
789
|
/**
|
|
790
|
-
* The History table stores all values per timestamp, table,
|
|
791
|
-
* column
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
* and DbChange to leverage a covering index. Hence, every value change
|
|
796
|
-
* has its timestamp, table, row, and column. In the future, we will
|
|
797
|
-
* rethink that and store history more efficiently.
|
|
798
|
-
*
|
|
799
|
-
* There is no need to use `OwnerId` on the client, because timestamps
|
|
800
|
-
* (which include a `NodeId`) are globally unique. The relay needs
|
|
801
|
-
* `OwnerId` because it hosts multiple apps/owners, but client storage
|
|
802
|
-
* represents one DB, and even when it hosts multiple owners, their
|
|
803
|
-
* timestamps remain unique.
|
|
790
|
+
* The History table stores all values per ownerId, timestamp, table, id,
|
|
791
|
+
* and column for conflict-free merging using last-write-win CRDT.
|
|
792
|
+
* Denormalizes Timestamp and DbChange for covering index performance.
|
|
793
|
+
* Time travel is available when last-write-win isn't desired. Future
|
|
794
|
+
* optimization will store history more efficiently.
|
|
804
795
|
*/
|
|
805
796
|
sql`
|
|
806
797
|
create table evolu_history (
|
|
807
|
-
"
|
|
798
|
+
"ownerId" blob not null,
|
|
808
799
|
"table" text not null,
|
|
809
|
-
"
|
|
800
|
+
"id" blob not null,
|
|
810
801
|
"column" text not null,
|
|
802
|
+
"timestamp" blob not null,
|
|
811
803
|
"value" any
|
|
812
804
|
)
|
|
813
805
|
strict;
|
|
814
806
|
`,
|
|
815
807
|
|
|
808
|
+
// Index for reading database changes by owner and timestamp.
|
|
809
|
+
// Timestamp always corresponds to a DbChange.
|
|
816
810
|
sql`
|
|
817
|
-
create index
|
|
811
|
+
create index evolu_history_ownerId_timestamp on evolu_history (
|
|
812
|
+
"ownerId",
|
|
813
|
+
"timestamp"
|
|
814
|
+
);
|
|
818
815
|
`,
|
|
819
816
|
|
|
820
817
|
sql`
|
|
821
|
-
create unique index
|
|
822
|
-
"
|
|
823
|
-
"column",
|
|
818
|
+
create unique index evolu_history_ownerId_table_id_column_timestampDesc on evolu_history (
|
|
819
|
+
"ownerId",
|
|
824
820
|
"table",
|
|
821
|
+
"id",
|
|
822
|
+
"column",
|
|
825
823
|
"timestamp" desc
|
|
826
824
|
);
|
|
827
825
|
`,
|
|
@@ -847,7 +845,14 @@ const initializeDb =
|
|
|
847
845
|
|
|
848
846
|
const result = deps.sqlite.exec(sql`
|
|
849
847
|
insert into evolu_owner
|
|
850
|
-
(
|
|
848
|
+
(
|
|
849
|
+
"mnemonic",
|
|
850
|
+
"id",
|
|
851
|
+
"encryptionKey",
|
|
852
|
+
"createdAt",
|
|
853
|
+
"writeKey",
|
|
854
|
+
"timestamp"
|
|
855
|
+
)
|
|
851
856
|
values
|
|
852
857
|
(
|
|
853
858
|
${ownerRow.mnemonic},
|
|
@@ -910,16 +915,18 @@ const applyMessages =
|
|
|
910
915
|
(
|
|
911
916
|
messages: ReadonlyArray<CrdtMessage>,
|
|
912
917
|
lastTimestamp: Timestamp,
|
|
913
|
-
options: { isMigrationFromVersion0?: boolean } = {},
|
|
914
918
|
): Result<void, SqliteError> => {
|
|
919
|
+
const ownerId = ownerIdToBinaryOwnerId(deps.ownerRowRef.get().id);
|
|
920
|
+
|
|
915
921
|
for (const message of messages) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (!apply1.ok) return apply1;
|
|
919
|
-
}
|
|
922
|
+
const result1 = applyMessageToAppTable(deps)(ownerId, message);
|
|
923
|
+
if (!result1.ok) return result1;
|
|
920
924
|
|
|
921
|
-
const
|
|
922
|
-
|
|
925
|
+
const result2 = applyMessageToTimestampAndHistoryTables(deps)(
|
|
926
|
+
ownerId,
|
|
927
|
+
message,
|
|
928
|
+
);
|
|
929
|
+
if (!result2.ok) return result2;
|
|
923
930
|
}
|
|
924
931
|
|
|
925
932
|
const timestamp = timestampToTimestampString(lastTimestamp);
|
|
@@ -933,8 +940,8 @@ const applyMessages =
|
|
|
933
940
|
};
|
|
934
941
|
|
|
935
942
|
const applyMessageToAppTable =
|
|
936
|
-
(deps: SqliteDep) =>
|
|
937
|
-
(message: CrdtMessage): Result<void, SqliteError> => {
|
|
943
|
+
(deps: SqliteDep & OwnerRowRefDep) =>
|
|
944
|
+
(ownerId: BinaryOwnerId, message: CrdtMessage): Result<void, SqliteError> => {
|
|
938
945
|
const date = new Date(message.timestamp.millis).toISOString();
|
|
939
946
|
const timestamp = timestampToBinaryTimestamp(message.timestamp);
|
|
940
947
|
|
|
@@ -942,28 +949,29 @@ const applyMessageToAppTable =
|
|
|
942
949
|
const result = deps.sqlite.exec(sql.prepared`
|
|
943
950
|
with
|
|
944
951
|
lastTimestamp as (
|
|
945
|
-
select timestamp
|
|
952
|
+
select "timestamp"
|
|
946
953
|
from evolu_history
|
|
947
954
|
where
|
|
948
|
-
"
|
|
949
|
-
and "column" = ${column}
|
|
955
|
+
"ownerId" = ${ownerId}
|
|
950
956
|
and "table" = ${message.change.table}
|
|
951
|
-
|
|
957
|
+
and "id" = ${message.change.id}
|
|
958
|
+
and "column" = ${column}
|
|
959
|
+
order by "timestamp" desc
|
|
952
960
|
limit 1
|
|
953
961
|
)
|
|
954
962
|
insert into ${sql.identifier(message.change.table)}
|
|
955
963
|
("id", ${sql.identifier(column)}, createdAt, updatedAt)
|
|
956
964
|
select ${message.change.id}, ${value}, ${date}, ${date}
|
|
957
965
|
where
|
|
958
|
-
(select timestamp from lastTimestamp) is null
|
|
959
|
-
or (select timestamp from lastTimestamp) < ${timestamp}
|
|
966
|
+
(select "timestamp" from lastTimestamp) is null
|
|
967
|
+
or (select "timestamp" from lastTimestamp) < ${timestamp}
|
|
960
968
|
on conflict ("id") do update
|
|
961
969
|
set
|
|
962
970
|
${sql.identifier(column)} = ${value},
|
|
963
971
|
updatedAt = ${date}
|
|
964
972
|
where
|
|
965
|
-
(select timestamp from lastTimestamp) is null
|
|
966
|
-
or (select timestamp from lastTimestamp) < ${timestamp};
|
|
973
|
+
(select "timestamp" from lastTimestamp) is null
|
|
974
|
+
or (select "timestamp" from lastTimestamp) < ${timestamp};
|
|
967
975
|
`);
|
|
968
976
|
|
|
969
977
|
if (!result.ok) return result;
|
|
@@ -973,10 +981,9 @@ const applyMessageToAppTable =
|
|
|
973
981
|
};
|
|
974
982
|
|
|
975
983
|
export const applyMessageToTimestampAndHistoryTables =
|
|
976
|
-
(deps: SqliteDep &
|
|
977
|
-
(message: CrdtMessage): Result<void, SqliteError> => {
|
|
984
|
+
(deps: SqliteDep & ClientStorageDep) =>
|
|
985
|
+
(ownerId: BinaryOwnerId, message: CrdtMessage): Result<void, SqliteError> => {
|
|
978
986
|
const timestamp = timestampToBinaryTimestamp(message.timestamp);
|
|
979
|
-
const ownerId = ownerIdToBinaryOwnerId(deps.ownerRowRef.get().id);
|
|
980
987
|
const id = idToBinaryId(message.change.id);
|
|
981
988
|
|
|
982
989
|
const result = deps.storage.insertTimestamp(ownerId, timestamp);
|
|
@@ -985,9 +992,16 @@ export const applyMessageToTimestampAndHistoryTables =
|
|
|
985
992
|
for (const [column, value] of Object.entries(message.change.values)) {
|
|
986
993
|
const result = deps.sqlite.exec(sql.prepared`
|
|
987
994
|
insert into evolu_history
|
|
988
|
-
("
|
|
995
|
+
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
989
996
|
values
|
|
990
|
-
(
|
|
997
|
+
(
|
|
998
|
+
${ownerId},
|
|
999
|
+
${message.change.table},
|
|
1000
|
+
${id},
|
|
1001
|
+
${column},
|
|
1002
|
+
${value},
|
|
1003
|
+
${timestamp}
|
|
1004
|
+
)
|
|
991
1005
|
on conflict do nothing;
|
|
992
1006
|
`);
|
|
993
1007
|
if (!result.ok) return result;
|
|
@@ -1063,11 +1077,11 @@ export const maybeMigrateToVersion0 =
|
|
|
1063
1077
|
const messagesRows = deps.sqlite.exec<{
|
|
1064
1078
|
timestamp: TimestampString;
|
|
1065
1079
|
table: Base64Url256;
|
|
1066
|
-
|
|
1080
|
+
id: Id;
|
|
1067
1081
|
column: Base64Url256;
|
|
1068
1082
|
value: SqliteValue;
|
|
1069
1083
|
}>(sql`
|
|
1070
|
-
select "timestamp", "table", "
|
|
1084
|
+
select "timestamp", "table", "id", "column", "value" from evolu_message;
|
|
1071
1085
|
`);
|
|
1072
1086
|
|
|
1073
1087
|
if (!messagesRows.ok) return messagesRows;
|
|
@@ -1083,7 +1097,7 @@ export const maybeMigrateToVersion0 =
|
|
|
1083
1097
|
const messages = messagesRows.value.rows.map((message) => ({
|
|
1084
1098
|
timestamp: timestampStringToTimestamp(message.timestamp),
|
|
1085
1099
|
change: {
|
|
1086
|
-
id: message.
|
|
1100
|
+
id: message.id,
|
|
1087
1101
|
table: message.table,
|
|
1088
1102
|
values: { [message.column]: message.value },
|
|
1089
1103
|
},
|
|
@@ -1231,16 +1245,16 @@ const createClientStorage =
|
|
|
1231
1245
|
return true;
|
|
1232
1246
|
},
|
|
1233
1247
|
|
|
1234
|
-
readDbChange: (
|
|
1248
|
+
readDbChange: (ownerId, timestamp) => {
|
|
1235
1249
|
const result = deps.sqlite.exec<{
|
|
1236
1250
|
table: Base64Url256;
|
|
1237
|
-
|
|
1251
|
+
id: BinaryId;
|
|
1238
1252
|
column: Base64Url256;
|
|
1239
1253
|
value: SqliteValue;
|
|
1240
1254
|
}>(sql`
|
|
1241
|
-
select "table", "
|
|
1255
|
+
select "table", "id", "column", "value"
|
|
1242
1256
|
from evolu_history
|
|
1243
|
-
where "timestamp" = ${timestamp};
|
|
1257
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
1244
1258
|
`);
|
|
1245
1259
|
if (!result.ok) {
|
|
1246
1260
|
deps.postMessage({ type: "onError", error: result.error });
|
|
@@ -1250,18 +1264,18 @@ const createClientStorage =
|
|
|
1250
1264
|
const { rows } = result.value;
|
|
1251
1265
|
assert(rows.length > 0, "Rows must not be empty");
|
|
1252
1266
|
|
|
1253
|
-
const { table,
|
|
1267
|
+
const { table, id } = rows[0];
|
|
1254
1268
|
const values: Record<string, SqliteValue> = {};
|
|
1255
1269
|
|
|
1256
1270
|
for (const r of rows) {
|
|
1257
1271
|
assert(r.table === table, "All rows must have the same table");
|
|
1258
|
-
assert(eqArrayNumber(r.
|
|
1272
|
+
assert(eqArrayNumber(r.id, id), "All rows must have the same Id");
|
|
1259
1273
|
values[r.column] = r.value;
|
|
1260
1274
|
}
|
|
1261
1275
|
|
|
1262
1276
|
const change: DbChange = {
|
|
1263
1277
|
table: rows[0].table,
|
|
1264
|
-
id: binaryIdToId(rows[0].
|
|
1278
|
+
id: binaryIdToId(rows[0].id),
|
|
1265
1279
|
values,
|
|
1266
1280
|
};
|
|
1267
1281
|
|