@evolu/common 6.0.1-preview.3 → 6.0.1-preview.5

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.
@@ -1,5 +1,6 @@
1
1
  import { Kysely, SelectQueryBuilder } from "kysely";
2
2
  import { pack } from "msgpackr";
3
+ import { assert } from "../Assert.js";
3
4
  import { mapObject, objectToEntries, ReadonlyRecord } from "../Object.js";
4
5
  import { err, ok, Result } from "../Result.js";
5
6
  import { SqliteBoolean, SqliteQueryOptions, SqliteValue } from "../Sqlite.js";
@@ -9,8 +10,7 @@ import {
9
10
  BrandType,
10
11
  createTypeErrorFormatter,
11
12
  DateIsoString,
12
- EvoluType,
13
- Id,
13
+ IdType,
14
14
  InferErrors,
15
15
  InferInput,
16
16
  InferType,
@@ -23,32 +23,20 @@ import {
23
23
  omit,
24
24
  optional,
25
25
  OptionalType,
26
- record,
27
26
  Type,
28
27
  TypeError,
29
- Unknown,
30
28
  } from "../Type.js";
31
29
  import { Simplify } from "../Types.js";
32
- import { DbSchema, DbTable } from "./Db.js";
30
+ import { DbSchema } from "./Db.js";
33
31
  import { createIndexes, DbIndexesBuilder } from "./Kysely.js";
34
32
  import { AppOwner, ShardOwner, SharedOwner } from "./Owner.js";
35
- import {
36
- Base64Url256,
37
- BinaryId,
38
- maxProtocolMessageRangesSize,
39
- } from "./Protocol.js";
33
+ import { BinaryId, maxProtocolMessageRangesSize } from "./Protocol.js";
40
34
  import { Query, Row } from "./Query.js";
41
35
  import { BinaryTimestamp } from "./Timestamp.js";
42
36
 
43
37
  /**
44
38
  * Defines the schema of an Evolu database.
45
39
  *
46
- * - Each top-level key represents a table name.
47
- * - The value for each table name is a record of column names mapped to their
48
- * respective data types, defined by {@link Type}.
49
- * - Each table must include a mandatory `id` column of type {@link Id}.
50
- * - No table may contain {@link DefaultColumns}.
51
- *
52
40
  * Table schema defines columns that are required for table rows. For not
53
41
  * required columns, use {@link nullOr}.
54
42
  *
@@ -82,11 +70,105 @@ import { BinaryTimestamp } from "./Timestamp.js";
82
70
  */
83
71
  export type EvoluSchema = ReadonlyRecord<
84
72
  string,
85
- ReadonlyRecord<string, Type<any, any, any, any, any>> & {
86
- readonly id: Type<any, any, any, any, any>;
87
- }
73
+ // TypeScript errors are cryptic so we use ValidateSchema.
74
+ ReadonlyRecord<string, Type<any, any, any, any, any>>
88
75
  >;
89
76
 
77
+ /**
78
+ * Validates an {@link EvoluSchema} at compile time, returning the first error
79
+ * found as a readable string literal type. This approach provides much clearer
80
+ * and more actionable TypeScript errors than the default, which are often hard
81
+ * to read.
82
+ *
83
+ * Validates the following schema requirements:
84
+ *
85
+ * 1. All tables must have an 'id' column
86
+ * 2. The 'id' column must be a branded ID type (created with id() function)
87
+ * 3. Tables cannot use default column names (createdAt, updatedAt, isDeleted)
88
+ * 4. All column types must be compatible with SQLite (extend SqliteValue)
89
+ */
90
+ export type ValidateSchema<S extends EvoluSchema> =
91
+ ValidateSchemaHasId<S> extends never
92
+ ? ValidateIdColumnType<S> extends never
93
+ ? ValidateNoDefaultColumns<S> extends never
94
+ ? ValidateColumnTypes<S> extends never
95
+ ? S
96
+ : ValidateColumnTypes<S>
97
+ : ValidateNoDefaultColumns<S>
98
+ : ValidateIdColumnType<S>
99
+ : ValidateSchemaHasId<S>;
100
+
101
+ export type ValidateSchemaHasId<S extends EvoluSchema> =
102
+ keyof S extends infer TableName
103
+ ? TableName extends keyof S
104
+ ? "id" extends keyof S[TableName]
105
+ ? never
106
+ : SchemaValidationError<`Table "${TableName & string}" is missing required id column.`>
107
+ : never
108
+ : never;
109
+
110
+ export type ValidateIdColumnType<S extends EvoluSchema> =
111
+ keyof S extends infer TableName
112
+ ? TableName extends keyof S
113
+ ? "id" extends keyof S[TableName]
114
+ ? S[TableName]["id"] extends IdType<any>
115
+ ? never
116
+ : SchemaValidationError<`Table "${TableName & string}" id column must be a branded ID type (created with id("${TableName & string}")).`>
117
+ : never
118
+ : never
119
+ : never;
120
+
121
+ export type ValidateNoDefaultColumns<S extends EvoluSchema> =
122
+ keyof S extends infer TableName
123
+ ? TableName extends keyof S
124
+ ? keyof S[TableName] extends infer ColumnName
125
+ ? ColumnName extends keyof S[TableName]
126
+ ? ColumnName extends "createdAt" | "updatedAt" | "isDeleted"
127
+ ? SchemaValidationError<`Table "${TableName & string}" uses default column name "${ColumnName & string}". Default columns (createdAt, updatedAt, isDeleted) are added automatically.`>
128
+ : never
129
+ : never
130
+ : never
131
+ : never
132
+ : never;
133
+
134
+ export type ValidateColumnTypes<S extends EvoluSchema> =
135
+ keyof S extends infer TableName
136
+ ? TableName extends keyof S
137
+ ? keyof S[TableName] extends infer ColumnName
138
+ ? ColumnName extends keyof S[TableName]
139
+ ? InferType<S[TableName][ColumnName]> extends SqliteValue
140
+ ? never
141
+ : SchemaValidationError<`Table "${TableName & string}" column "${ColumnName & string}" type is not compatible with SQLite. Column types must extend SqliteValue (string, number, Uint8Array, or null).`>
142
+ : never
143
+ : never
144
+ : never
145
+ : never;
146
+
147
+ /** Schema validation error that shows clear, readable messages */
148
+ export type SchemaValidationError<Message extends string> =
149
+ `❌ Schema Error: ${Message}`;
150
+
151
+ export const evoluSchemaToDbSchema = (
152
+ schema: EvoluSchema,
153
+ indexes?: DbIndexesBuilder,
154
+ ): DbSchema => {
155
+ const tables = objectToEntries(schema).map(([tableName, table]) => ({
156
+ name: tableName,
157
+ columns: objectToEntries(table)
158
+ .filter(([k]) => k !== "id")
159
+ .map(([k]) => k),
160
+ }));
161
+
162
+ const dbSchema = { tables, indexes: createIndexes(indexes) };
163
+
164
+ assert(
165
+ DbSchema.is(dbSchema),
166
+ "Invalid EvoluSchema: Table and column names must use only characters A-Za-z0-9_- and be at most 256 characters long.",
167
+ );
168
+
169
+ return dbSchema;
170
+ };
171
+
90
172
  export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
91
173
  queryCallback: (
92
174
  db: Pick<
@@ -123,116 +205,6 @@ export const DefaultColumns = object({
123
205
  });
124
206
  export type DefaultColumns = typeof DefaultColumns.Type;
125
207
 
126
- const isDefaultColumnName = (value: string): boolean =>
127
- value === "createdAt" || value === "updatedAt" || value === "isDeleted";
128
-
129
- /**
130
- * Valid {@link EvoluSchema}.
131
- *
132
- * - Table and column names must be Base64Url strings.
133
- * - Each table must include an `id` column of type {@link Id}.
134
- * - Default column names (`createdAt`, `updatedAt`, `isDeleted`) are not allowed.
135
- */
136
- export const ValidEvoluSchema = brand(
137
- "ValidEvoluSchema",
138
- record(
139
- Base64Url256,
140
- object({ id: EvoluType }, record(Base64Url256, Unknown)),
141
- ),
142
- (value) => {
143
- for (const tableName in value) {
144
- for (const columnName in value[tableName as never]) {
145
- if (isDefaultColumnName(columnName)) {
146
- return err<ValidEvoluSchemaError>({
147
- type: "ValidEvoluSchema",
148
- value,
149
- reason: {
150
- kind: "DefaultColumnError",
151
- tableName,
152
- columnName,
153
- },
154
- });
155
- }
156
- }
157
- }
158
-
159
- return ok(value);
160
- },
161
- );
162
-
163
- export type ValidEvoluSchema = typeof ValidEvoluSchema.Type;
164
-
165
- export interface ValidEvoluSchemaError extends TypeError<"ValidEvoluSchema"> {
166
- readonly reason: {
167
- kind: "DefaultColumnError";
168
- tableName: string;
169
- columnName: string;
170
- };
171
- }
172
-
173
- /**
174
- * Asserts that the given value is {@link ValidEvoluSchema}.
175
- *
176
- * Throws an error if the value is not a valid Evolu schema.
177
- */
178
- export const assertValidEvoluSchema = (value: unknown): ValidEvoluSchema => {
179
- const validEvoluSchema = ValidEvoluSchema.fromUnknown(value);
180
- if (!validEvoluSchema.ok) {
181
- const message = formatValidEvoluSchemaError(validEvoluSchema.error);
182
- throw new Error(`Invalid Evolu schema: ${message}`);
183
- }
184
- return validEvoluSchema.value;
185
- };
186
-
187
- const formatValidEvoluSchemaError = (
188
- error: typeof ValidEvoluSchema.Error | typeof ValidEvoluSchema.ParentError,
189
- ): string => {
190
- if (error.type === "Record") {
191
- if (error.reason.kind === "Key") {
192
- return `The table "${error.reason.key}" has invalid name. A table name must be Base64Url256 string (A-Z, a-z, 0-9, -, _).`;
193
- }
194
-
195
- if (
196
- error.reason.kind === "Value" &&
197
- error.reason.error.reason.kind === "Props" &&
198
- error.reason.error.reason.errors.id?.type === "EvoluType"
199
- ) {
200
- return `The table "${error.reason.key}" has invalid ID column. Check examples.`;
201
- }
202
-
203
- if (
204
- error.reason.kind === "Value" &&
205
- error.reason.error.reason.kind === "IndexKey"
206
- ) {
207
- 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, -, _).`;
208
- }
209
- }
210
-
211
- if (error.type === "ValidEvoluSchema") {
212
- return `The table "${error.reason.tableName}" uses reserved column name "${error.reason.columnName}". Reserved column names are: createdAt, updatedAt, isDeleted.`;
213
- }
214
-
215
- return JSON.stringify(error, null, 2);
216
- };
217
-
218
- export const validEvoluSchemaToDbSchema = (
219
- validEvoluSchema: ValidEvoluSchema,
220
- indexes?: DbIndexesBuilder,
221
- ): DbSchema => {
222
- const tables = objectToEntries(validEvoluSchema).map(
223
- ([tableName, table]): DbTable => ({
224
- name: tableName,
225
- columns: objectToEntries(table)
226
- .filter(([k]) => k !== "id")
227
- .map(([k]) => k as Base64Url256),
228
- }),
229
- );
230
- return {
231
- tables,
232
- indexes: createIndexes(indexes),
233
- };
234
- };
235
-
236
208
  export type MutationKind = "insert" | "update" | "upsert";
237
209
 
238
210
  export type Mutation<S extends EvoluSchema, Kind extends MutationKind> = <