rwsdk 1.0.0-beta.27-test.20251116215153 → 1.0.0-beta.28
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/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
- package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +34 -20
- package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
- package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
- package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
- package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +102 -1
- package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
- package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
- package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.js +167 -28
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +43 -9
- package/package.json +1 -1
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { CheckConstraintNode, Expression, ForeignKeyConstraintBuilder, PrimaryKeyConstraintNode, sql, UniqueConstraintNode } from "kysely";
|
|
2
2
|
import { AddColumnOp, AlterColumnOp, AlterOperation, DropColumnOp, ExecutedBuilder, ModifyColumnOp, RenameColumnOp, SqlToTsType } from "../utils";
|
|
3
3
|
import { AlterColumnBuilderCallback } from "./alterColumn";
|
|
4
|
-
import { ColumnDefinitionBuilder } from "./columnDefinition";
|
|
4
|
+
import { ColumnDefinitionBuilder, ColumnDescriptor } from "./columnDefinition";
|
|
5
5
|
type DataTypeExpression = string | typeof sql;
|
|
6
|
+
type InitialDescriptor<TType> = {
|
|
7
|
+
tsType: TType;
|
|
8
|
+
isNullable: true;
|
|
9
|
+
hasDefault: false;
|
|
10
|
+
isAutoIncrement: false;
|
|
11
|
+
};
|
|
6
12
|
interface CheckConstraintBuilder {
|
|
7
13
|
$call<T>(func: (qb: this) => T): T;
|
|
8
14
|
toOperationNode(): CheckConstraintNode;
|
|
@@ -31,14 +37,18 @@ export interface AlterTableBuilder<TName extends string, TOps extends AlterOpera
|
|
|
31
37
|
readonly __renamedFrom: TName;
|
|
32
38
|
};
|
|
33
39
|
setSchema(newSchema: string): AlterTableBuilder<TName, TOps>;
|
|
34
|
-
addColumn<K extends string, T extends DataTypeExpression>(name: K, type: T
|
|
40
|
+
addColumn<K extends string, T extends DataTypeExpression>(name: K, type: T): AlterTableBuilder<TName, [
|
|
41
|
+
...TOps,
|
|
42
|
+
AddColumnOp<K, T, InitialDescriptor<SqlToTsType<T>>>
|
|
43
|
+
]>;
|
|
44
|
+
addColumn<K extends string, T extends DataTypeExpression, TDescriptor extends ColumnDescriptor>(name: K, type: T, build: (col: ColumnDefinitionBuilder<InitialDescriptor<SqlToTsType<T>>>) => ColumnDefinitionBuilder<TDescriptor>): AlterTableBuilder<TName, [...TOps, AddColumnOp<K, T, TDescriptor>]>;
|
|
35
45
|
dropColumn<K extends string>(name: K): AlterTableBuilder<TName, [...TOps, DropColumnOp<K>]>;
|
|
36
46
|
renameColumn<KFrom extends string, KTo extends string>(from: KFrom, to: KTo): AlterTableBuilder<TName, [...TOps, RenameColumnOp<KFrom, KTo>]>;
|
|
37
47
|
alterColumn<K extends string, const TCallback extends AlterColumnBuilderCallback>(column: K, alteration: TCallback): AlterTableBuilder<TName, [
|
|
38
48
|
...TOps,
|
|
39
49
|
AlterColumnOp<K, ReturnType<TCallback>["__alteration"]>
|
|
40
50
|
]>;
|
|
41
|
-
modifyColumn<K extends string, T extends DataTypeExpression>(column: K, type: T, build?: (col: ColumnDefinitionBuilder<SqlToTsType<T
|
|
51
|
+
modifyColumn<K extends string, T extends DataTypeExpression, TDescriptor extends ColumnDescriptor>(column: K, type: T, build?: (col: ColumnDefinitionBuilder<InitialDescriptor<SqlToTsType<T>>>) => ColumnDefinitionBuilder<TDescriptor>): AlterTableBuilder<TName, [...TOps, ModifyColumnOp<K, T, TDescriptor>]>;
|
|
42
52
|
addUniqueConstraint(constraintName: string, columns: string[], build?: (builder: UniqueConstraintBuilder) => UniqueConstraintBuilder): AlterTableBuilder<TName, TOps>;
|
|
43
53
|
addPrimaryKeyConstraint(constraintName: string, columns: string[], build?: (builder: PrimaryKeyConstraintBuilder) => PrimaryKeyConstraintBuilder): AlterTableBuilder<TName, TOps>;
|
|
44
54
|
addCheckConstraint(constraintName: string, checkExpression: Expression<any>, build?: (builder: CheckConstraintBuilder) => CheckConstraintBuilder): AlterTableBuilder<TName, TOps>;
|
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
import { ColumnDefinitionNode, Expression, sql } from "kysely";
|
|
2
2
|
type DefaultValueExpression = string | number | boolean | null | ReturnType<typeof sql>;
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
3
|
+
export type ColumnDescriptor = {
|
|
4
|
+
tsType: any;
|
|
5
|
+
isNullable: boolean;
|
|
6
|
+
hasDefault: boolean;
|
|
7
|
+
isAutoIncrement: boolean;
|
|
8
|
+
};
|
|
9
|
+
export interface ColumnDefinitionBuilder<TDescriptor extends ColumnDescriptor> {
|
|
10
|
+
autoIncrement(): ColumnDefinitionBuilder<{
|
|
11
|
+
[K in keyof TDescriptor]: K extends "isAutoIncrement" ? true : TDescriptor[K];
|
|
12
|
+
}>;
|
|
13
|
+
identity(): ColumnDefinitionBuilder<TDescriptor>;
|
|
14
|
+
primaryKey(): ColumnDefinitionBuilder<{
|
|
15
|
+
[K in keyof TDescriptor]: K extends "isNullable" ? false : TDescriptor[K];
|
|
16
|
+
}>;
|
|
17
|
+
references(ref: string): ColumnDefinitionBuilder<TDescriptor>;
|
|
18
|
+
onDelete(onDelete: "no action" | "restrict" | "cascade" | "set null" | "set default"): ColumnDefinitionBuilder<TDescriptor>;
|
|
19
|
+
onUpdate(onUpdate: "no action" | "restrict" | "cascade" | "set null" | "set default"): ColumnDefinitionBuilder<TDescriptor>;
|
|
20
|
+
unique(): ColumnDefinitionBuilder<TDescriptor>;
|
|
21
|
+
notNull(): ColumnDefinitionBuilder<{
|
|
22
|
+
[K in keyof TDescriptor]: K extends "isNullable" ? false : TDescriptor[K];
|
|
23
|
+
}>;
|
|
24
|
+
unsigned(): ColumnDefinitionBuilder<TDescriptor>;
|
|
25
|
+
defaultTo(value: DefaultValueExpression): ColumnDefinitionBuilder<{
|
|
26
|
+
[K in keyof TDescriptor]: K extends "isNullable" ? false : K extends "hasDefault" ? true : TDescriptor[K];
|
|
27
|
+
}>;
|
|
28
|
+
check(expression: Expression<any>): ColumnDefinitionBuilder<TDescriptor>;
|
|
29
|
+
generatedAlwaysAs(expression: Expression<any>): ColumnDefinitionBuilder<TDescriptor>;
|
|
30
|
+
generatedAlwaysAsIdentity(): ColumnDefinitionBuilder<TDescriptor>;
|
|
31
|
+
generatedByDefaultAsIdentity(): ColumnDefinitionBuilder<TDescriptor>;
|
|
32
|
+
stored(): ColumnDefinitionBuilder<TDescriptor>;
|
|
33
|
+
modifyFront(modifier: Expression<any>): ColumnDefinitionBuilder<TDescriptor>;
|
|
34
|
+
nullsNotDistinct(): ColumnDefinitionBuilder<TDescriptor>;
|
|
35
|
+
ifNotExists(): ColumnDefinitionBuilder<TDescriptor>;
|
|
36
|
+
modifyEnd(modifier: Expression<any>): ColumnDefinitionBuilder<TDescriptor>;
|
|
23
37
|
$call<T>(func: (qb: this) => T): T;
|
|
24
38
|
toOperationNode(): ColumnDefinitionNode;
|
|
25
39
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CheckConstraintNode, CompiledQuery, CreateTableNode, Expression, ForeignKeyConstraintBuilder, PrimaryKeyConstraintNode, UniqueConstraintNode } from "kysely";
|
|
2
2
|
import { ExecutedBuilder, Prettify, SqlToTsType } from "../utils";
|
|
3
|
-
import { ColumnDefinitionBuilder } from "./columnDefinition";
|
|
3
|
+
import { ColumnDefinitionBuilder, ColumnDescriptor } from "./columnDefinition";
|
|
4
4
|
interface CheckConstraintBuilder {
|
|
5
5
|
$call<T>(func: (qb: this) => T): T;
|
|
6
6
|
toOperationNode(): CheckConstraintNode;
|
|
@@ -22,13 +22,20 @@ interface PrimaryKeyConstraintBuilder {
|
|
|
22
22
|
$call<T>(func: (qb: this) => T): T;
|
|
23
23
|
toOperationNode(): PrimaryKeyConstraintNode;
|
|
24
24
|
}
|
|
25
|
+
type InitialDescriptor<TType> = {
|
|
26
|
+
tsType: TType;
|
|
27
|
+
isNullable: true;
|
|
28
|
+
hasDefault: false;
|
|
29
|
+
isAutoIncrement: false;
|
|
30
|
+
};
|
|
25
31
|
export interface CreateTableBuilder<TName extends string, TSchema extends Record<string, any> = {}> {
|
|
26
32
|
readonly __tableName: TName;
|
|
27
33
|
readonly __addedColumns: TSchema;
|
|
28
34
|
temporary(): CreateTableBuilder<TName, TSchema>;
|
|
29
35
|
onCommit(onCommit: "preserve rows" | "delete rows" | "drop"): CreateTableBuilder<TName, TSchema>;
|
|
30
36
|
ifNotExists(): CreateTableBuilder<TName, TSchema>;
|
|
31
|
-
addColumn<K extends string, T extends string>(name: K, type: T
|
|
37
|
+
addColumn<K extends string, T extends string>(name: K, type: T): CreateTableBuilder<TName, Prettify<(TSchema extends Record<string, any> ? TSchema : {}) & Record<K, InitialDescriptor<SqlToTsType<T>>>>>;
|
|
38
|
+
addColumn<K extends string, T extends string, TDescriptor extends ColumnDescriptor>(name: K, type: T, build: (col: ColumnDefinitionBuilder<InitialDescriptor<SqlToTsType<T>>>) => ColumnDefinitionBuilder<TDescriptor>): CreateTableBuilder<TName, Prettify<(TSchema extends Record<string, any> ? TSchema : {}) & Record<K, TDescriptor>>>;
|
|
32
39
|
addUniqueConstraint(constraintName: string, columns: (keyof TSchema)[], build?: (builder: UniqueConstraintBuilder) => UniqueConstraintBuilder): CreateTableBuilder<TName, TSchema>;
|
|
33
40
|
addPrimaryKeyConstraint(constraintName: string, columns: (keyof TSchema)[], build?: (builder: PrimaryKeyConstraintBuilder) => PrimaryKeyConstraintBuilder): CreateTableBuilder<TName, TSchema>;
|
|
34
41
|
addCheckConstraint(constraintName: string, checkExpression: Expression<any>, build?: (builder: CheckConstraintBuilder) => CheckConstraintBuilder): CreateTableBuilder<TName, TSchema>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Kysely } from "kysely";
|
|
1
|
+
import { Generated, Kysely } from "kysely";
|
|
2
2
|
import { AlterTableBuilder } from "./builders/alterTable";
|
|
3
3
|
import { CreateTableBuilder } from "./builders/createTable";
|
|
4
4
|
import { DropTableBuilder } from "./builders/dropTable";
|
|
5
5
|
import { SchemaBuilder } from "./builders/schema";
|
|
6
6
|
import { ExecutedBuilder, Prettify, ProcessAlteredTable, UnionToTuple } from "./utils";
|
|
7
|
+
import { ColumnDescriptor } from "./builders/columnDefinition";
|
|
7
8
|
export interface InferenceBuilder {
|
|
8
9
|
schema: SchemaBuilder;
|
|
9
10
|
}
|
|
@@ -23,5 +24,18 @@ type ApplyBuilders<TSchema, TBuildersTuple> = TBuildersTuple extends [
|
|
|
23
24
|
...infer TRest
|
|
24
25
|
] ? ApplyBuilders<ApplyBuilder<TSchema, THead>, TRest> : TSchema;
|
|
25
26
|
type ProcessMigrations<TMigrations extends Migrations, TKeys, TSchema = {}> = TKeys extends [infer THeadKey, ...infer TRestKeys] ? THeadKey extends keyof TMigrations ? ProcessMigrations<TMigrations, TRestKeys, ApplyBuilders<TSchema, UnionToTuple<BuildersFromMigration<TMigrations[THeadKey]>>>> : TSchema : TSchema;
|
|
26
|
-
|
|
27
|
+
type TableToSelectType<TTable> = Prettify<{
|
|
28
|
+
[K in keyof TTable]: TTable[K] extends ColumnDescriptor ? TTable[K]["isNullable"] extends true ? TTable[K]["tsType"] | null : TTable[K]["tsType"] : TTable[K];
|
|
29
|
+
}>;
|
|
30
|
+
type TableToKyselySchema<TTable> = Prettify<{
|
|
31
|
+
[K in keyof TTable]: TTable[K] extends ColumnDescriptor ? TTable[K]["hasDefault"] extends true ? Generated<TTable[K]["isNullable"] extends true ? TTable[K]["tsType"] | null : TTable[K]["tsType"]> : TTable[K]["isAutoIncrement"] extends true ? Generated<TTable[K]["tsType"]> : TTable[K]["isNullable"] extends true ? TTable[K]["tsType"] | null : TTable[K]["tsType"] : TTable[K];
|
|
32
|
+
}>;
|
|
33
|
+
type DatabaseWithDescriptors<TMigrations extends Migrations = Migrations> = ProcessMigrations<TMigrations, UnionToTuple<keyof TMigrations>>;
|
|
34
|
+
export type Database<TMigrations extends Migrations = Migrations> = Prettify<{
|
|
35
|
+
[K in keyof DatabaseWithDescriptors<TMigrations>]: TableToSelectType<DatabaseWithDescriptors<TMigrations>[K]>;
|
|
36
|
+
} & {
|
|
37
|
+
__kyselySchema: {
|
|
38
|
+
[K in keyof DatabaseWithDescriptors<TMigrations>]: TableToKyselySchema<DatabaseWithDescriptors<TMigrations>[K]>;
|
|
39
|
+
};
|
|
40
|
+
}>;
|
|
27
41
|
export {};
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
},
|
|
98
98
|
},
|
|
99
99
|
};
|
|
100
|
-
//(_test:
|
|
100
|
+
//(_test: ExpectDb<Actual, Expected>) => {};
|
|
101
101
|
};
|
|
102
102
|
(_it = "alterTable addUniqueConstraint") => {
|
|
103
103
|
const migrations = {
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
},
|
|
124
124
|
},
|
|
125
125
|
};
|
|
126
|
-
//(_test:
|
|
126
|
+
//(_test: ExpectDb<Actual, Expected>) => {};
|
|
127
127
|
};
|
|
128
128
|
(_it = "alterTable drop column") => {
|
|
129
129
|
const migrations = {
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
up: async (db) => [
|
|
154
154
|
await db.schema
|
|
155
155
|
.createTable("users")
|
|
156
|
-
.addColumn("id", "integer")
|
|
156
|
+
.addColumn("id", "integer", (c) => c.primaryKey().autoIncrement())
|
|
157
157
|
.execute(),
|
|
158
158
|
],
|
|
159
159
|
},
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
return [
|
|
243
243
|
await db.schema
|
|
244
244
|
.createTable("users")
|
|
245
|
-
.addColumn("id", "integer")
|
|
245
|
+
.addColumn("id", "integer", (col) => col.notNull())
|
|
246
246
|
.execute(),
|
|
247
247
|
];
|
|
248
248
|
},
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
return [
|
|
272
272
|
await db.schema
|
|
273
273
|
.createTable("users")
|
|
274
|
-
.addColumn("id", "integer")
|
|
274
|
+
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
|
|
275
275
|
.execute(),
|
|
276
276
|
];
|
|
277
277
|
},
|
|
@@ -357,4 +357,79 @@
|
|
|
357
357
|
};
|
|
358
358
|
(_test) => { };
|
|
359
359
|
};
|
|
360
|
+
(_it = "alterTable addColumn with notNull") => {
|
|
361
|
+
const migrations = {
|
|
362
|
+
"0": {
|
|
363
|
+
async up(db) {
|
|
364
|
+
return [
|
|
365
|
+
await db.schema
|
|
366
|
+
.createTable("users")
|
|
367
|
+
.addColumn("id", "integer", (col) => col.primaryKey())
|
|
368
|
+
.execute(),
|
|
369
|
+
];
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
"1": {
|
|
373
|
+
async up(db) {
|
|
374
|
+
return [
|
|
375
|
+
await db.schema
|
|
376
|
+
.alterTable("users")
|
|
377
|
+
.addColumn("email", "text", (col) => col.notNull())
|
|
378
|
+
.execute(),
|
|
379
|
+
];
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
(_test) => { };
|
|
384
|
+
};
|
|
385
|
+
(_it = "alterTable modifyColumn with notNull") => {
|
|
386
|
+
const migrations = {
|
|
387
|
+
"0": {
|
|
388
|
+
async up(db) {
|
|
389
|
+
return [
|
|
390
|
+
await db.schema
|
|
391
|
+
.createTable("products")
|
|
392
|
+
.addColumn("price", "real")
|
|
393
|
+
.execute(),
|
|
394
|
+
];
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
"1": {
|
|
398
|
+
async up(db) {
|
|
399
|
+
return [
|
|
400
|
+
await db.schema
|
|
401
|
+
.alterTable("products")
|
|
402
|
+
.modifyColumn("price", "real", (col) => col.notNull())
|
|
403
|
+
.execute(),
|
|
404
|
+
];
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
(_test) => { };
|
|
409
|
+
};
|
|
410
|
+
(_it = "alterTable modifyColumn nullable to non-nullable") => {
|
|
411
|
+
const migrations = {
|
|
412
|
+
"0": {
|
|
413
|
+
async up(db) {
|
|
414
|
+
return [
|
|
415
|
+
await db.schema
|
|
416
|
+
.createTable("orders")
|
|
417
|
+
.addColumn("status", "text")
|
|
418
|
+
.execute(),
|
|
419
|
+
];
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
"1": {
|
|
423
|
+
async up(db) {
|
|
424
|
+
return [
|
|
425
|
+
await db.schema
|
|
426
|
+
.alterTable("orders")
|
|
427
|
+
.modifyColumn("status", "text", (col) => col.notNull().defaultTo("pending"))
|
|
428
|
+
.execute(),
|
|
429
|
+
];
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
(_test) => { };
|
|
434
|
+
};
|
|
360
435
|
export {};
|
|
@@ -22,9 +22,11 @@ import { sql } from "kysely";
|
|
|
22
22
|
await db.schema
|
|
23
23
|
.createTable("users")
|
|
24
24
|
.addColumn("username", "text", (col) => col.notNull())
|
|
25
|
-
.addColumn("age", "integer", (col) => col.defaultTo(18))
|
|
26
25
|
.addColumn("active", "boolean", (col) => col.defaultTo(true))
|
|
27
26
|
.addColumn("anotherBoolean", "boolean", (col) => col.defaultTo(sql `true`))
|
|
27
|
+
.addColumn("email", "text", (col) => col)
|
|
28
|
+
.addColumn("favoriteColor", "text", (col) => col.unique())
|
|
29
|
+
.addColumn("name", "text", (col) => col.defaultTo("John Doe"))
|
|
28
30
|
.execute(),
|
|
29
31
|
];
|
|
30
32
|
},
|
|
@@ -32,3 +34,102 @@ import { sql } from "kysely";
|
|
|
32
34
|
};
|
|
33
35
|
(_test) => { };
|
|
34
36
|
};
|
|
37
|
+
(_it = "createTable column without callback is nullable") => {
|
|
38
|
+
const migrations = {
|
|
39
|
+
"001_init": {
|
|
40
|
+
async up(db) {
|
|
41
|
+
return [
|
|
42
|
+
await db.schema
|
|
43
|
+
.createTable("posts")
|
|
44
|
+
.addColumn("title", "text")
|
|
45
|
+
.addColumn("body", "text")
|
|
46
|
+
.execute(),
|
|
47
|
+
];
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
(_test) => { };
|
|
52
|
+
};
|
|
53
|
+
(_it = "createTable with primaryKey is non-nullable") => {
|
|
54
|
+
const migrations = {
|
|
55
|
+
"001_init": {
|
|
56
|
+
async up(db) {
|
|
57
|
+
return [
|
|
58
|
+
await db.schema
|
|
59
|
+
.createTable("users")
|
|
60
|
+
.addColumn("id", "integer", (col) => col.primaryKey())
|
|
61
|
+
.addColumn("email", "text", (col) => col.notNull())
|
|
62
|
+
.execute(),
|
|
63
|
+
];
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
(_test) => { };
|
|
68
|
+
};
|
|
69
|
+
(_it = "createTable with unique but no notNull is nullable") => {
|
|
70
|
+
const migrations = {
|
|
71
|
+
"001_init": {
|
|
72
|
+
async up(db) {
|
|
73
|
+
return [
|
|
74
|
+
await db.schema
|
|
75
|
+
.createTable("products")
|
|
76
|
+
.addColumn("sku", "text", (col) => col.unique())
|
|
77
|
+
.addColumn("name", "text", (col) => col)
|
|
78
|
+
.execute(),
|
|
79
|
+
];
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
(_test) => { };
|
|
84
|
+
};
|
|
85
|
+
(_it = "defaultTo makes columns non-nullable in Database type") => {
|
|
86
|
+
const migrations = {
|
|
87
|
+
"001_init": {
|
|
88
|
+
async up(db) {
|
|
89
|
+
return [
|
|
90
|
+
await db.schema
|
|
91
|
+
.createTable("users")
|
|
92
|
+
.addColumn("status", "text", (col) => col.defaultTo("active"))
|
|
93
|
+
.addColumn("count", "integer", (col) => col.defaultTo(0))
|
|
94
|
+
.execute(),
|
|
95
|
+
];
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
(_test) => { };
|
|
100
|
+
};
|
|
101
|
+
// --- Insert/Update Type Tests ---
|
|
102
|
+
(_it = "makes autoIncrement columns optional on insert") => {
|
|
103
|
+
const migrations = {
|
|
104
|
+
"001_init": {
|
|
105
|
+
async up(db) {
|
|
106
|
+
return [
|
|
107
|
+
await db.schema
|
|
108
|
+
.createTable("users")
|
|
109
|
+
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
|
|
110
|
+
.addColumn("username", "text", (col) => col.notNull())
|
|
111
|
+
.execute(),
|
|
112
|
+
];
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
const db = {};
|
|
117
|
+
db.insertInto("users").values({ username: "test" });
|
|
118
|
+
};
|
|
119
|
+
(_it = "makes defaultTo columns optional on insert") => {
|
|
120
|
+
const migrations = {
|
|
121
|
+
"001_init": {
|
|
122
|
+
async up(db) {
|
|
123
|
+
return [
|
|
124
|
+
await db.schema
|
|
125
|
+
.createTable("users")
|
|
126
|
+
.addColumn("username", "text", (col) => col.notNull())
|
|
127
|
+
.addColumn("status", "text", (col) => col.notNull().defaultTo("active"))
|
|
128
|
+
.execute(),
|
|
129
|
+
];
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const db = {};
|
|
134
|
+
db.insertInto("users").values({ username: "test" });
|
|
135
|
+
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { sql } from "kysely";
|
|
2
|
+
import { ColumnDescriptor } from "./builders/columnDefinition";
|
|
2
3
|
type DataTypeExpression = string | typeof sql;
|
|
3
|
-
export type AddColumnOp<K extends string, T extends DataTypeExpression> = {
|
|
4
|
+
export type AddColumnOp<K extends string, T extends DataTypeExpression, TDescriptor extends ColumnDescriptor> = {
|
|
4
5
|
op: "addColumn";
|
|
5
6
|
name: K;
|
|
6
7
|
type: T;
|
|
8
|
+
descriptor: TDescriptor;
|
|
7
9
|
};
|
|
8
10
|
export type DropColumnOp<K extends string> = {
|
|
9
11
|
op: "dropColumn";
|
|
@@ -14,10 +16,11 @@ export type RenameColumnOp<KFrom extends string, KTo extends string> = {
|
|
|
14
16
|
from: KFrom;
|
|
15
17
|
to: KTo;
|
|
16
18
|
};
|
|
17
|
-
export type ModifyColumnOp<K extends string, T extends DataTypeExpression> = {
|
|
19
|
+
export type ModifyColumnOp<K extends string, T extends DataTypeExpression, TDescriptor extends ColumnDescriptor> = {
|
|
18
20
|
op: "modifyColumn";
|
|
19
21
|
name: K;
|
|
20
22
|
type: T;
|
|
23
|
+
descriptor: TDescriptor;
|
|
21
24
|
};
|
|
22
25
|
export type Alteration = {
|
|
23
26
|
kind: "setDataType";
|
|
@@ -37,7 +40,7 @@ export type AlterColumnOp<K extends string, TAlteration extends Alteration> = {
|
|
|
37
40
|
name: K;
|
|
38
41
|
alteration: TAlteration;
|
|
39
42
|
};
|
|
40
|
-
export type AlterOperation = AddColumnOp<any, any> | DropColumnOp<any> | RenameColumnOp<any, any> | AlterColumnOp<any, any> | ModifyColumnOp<any, any>;
|
|
43
|
+
export type AlterOperation = AddColumnOp<any, any, any> | DropColumnOp<any> | RenameColumnOp<any, any> | AlterColumnOp<any, any> | ModifyColumnOp<any, any, any>;
|
|
41
44
|
export type SqlToTsType<T extends string | typeof sql> = T extends "text" ? string : T extends "integer" ? number : T extends "blob" ? Uint8Array : T extends "real" ? number : T extends "boolean" ? boolean : T extends typeof sql ? any : never;
|
|
42
45
|
export type Prettify<T> = {
|
|
43
46
|
[K in keyof T]: T[K];
|
|
@@ -57,17 +60,64 @@ export type Cast<A, B> = A extends B ? A : B;
|
|
|
57
60
|
/**
|
|
58
61
|
* Applies a single alteration operation to a schema.
|
|
59
62
|
*/
|
|
60
|
-
type ApplyOp<TSchema, THeadOp> = THeadOp extends AddColumnOp<infer K, infer
|
|
61
|
-
[P in K]:
|
|
63
|
+
type ApplyOp<TSchema, THeadOp> = THeadOp extends AddColumnOp<infer K, any, infer TDescriptor> ? Prettify<TSchema & {
|
|
64
|
+
[P in K]: TDescriptor;
|
|
62
65
|
}> : THeadOp extends DropColumnOp<infer K> ? Omit<TSchema, K> : THeadOp extends RenameColumnOp<infer KFrom, infer KTo> ? KFrom extends keyof TSchema ? Prettify<Omit<TSchema, KFrom> & {
|
|
63
66
|
[P in KTo]: TSchema[KFrom];
|
|
64
|
-
}> : TSchema : THeadOp extends AlterColumnOp<infer K, infer TAlt> ? TAlt extends {
|
|
67
|
+
}> : TSchema : THeadOp extends AlterColumnOp<infer K, infer TAlt> ? K extends keyof TSchema ? TAlt extends {
|
|
65
68
|
kind: "setDataType";
|
|
66
69
|
dataType: infer DT extends string;
|
|
67
70
|
} ? Prettify<Omit<TSchema, K> & {
|
|
68
|
-
[P in K]:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
[P in K]: {
|
|
72
|
+
tsType: SqlToTsType<DT>;
|
|
73
|
+
isNullable: TSchema[K] extends {
|
|
74
|
+
isNullable: infer N;
|
|
75
|
+
} ? N : true;
|
|
76
|
+
hasDefault: TSchema[K] extends {
|
|
77
|
+
hasDefault: infer D;
|
|
78
|
+
} ? D : false;
|
|
79
|
+
isAutoIncrement: TSchema[K] extends {
|
|
80
|
+
isAutoIncrement: infer A;
|
|
81
|
+
} ? A : false;
|
|
82
|
+
};
|
|
83
|
+
}> : TAlt extends {
|
|
84
|
+
kind: "setDefault";
|
|
85
|
+
} ? Prettify<Omit<TSchema, K> & {
|
|
86
|
+
[P in K]: TSchema[K] extends ColumnDescriptor ? {
|
|
87
|
+
tsType: TSchema[K]["tsType"];
|
|
88
|
+
isNullable: false;
|
|
89
|
+
hasDefault: true;
|
|
90
|
+
isAutoIncrement: TSchema[K]["isAutoIncrement"];
|
|
91
|
+
} : TSchema[K];
|
|
92
|
+
}> : TAlt extends {
|
|
93
|
+
kind: "dropDefault";
|
|
94
|
+
} ? Prettify<Omit<TSchema, K> & {
|
|
95
|
+
[P in K]: TSchema[K] extends ColumnDescriptor ? {
|
|
96
|
+
tsType: TSchema[K]["tsType"];
|
|
97
|
+
isNullable: TSchema[K]["isNullable"];
|
|
98
|
+
hasDefault: false;
|
|
99
|
+
isAutoIncrement: TSchema[K]["isAutoIncrement"];
|
|
100
|
+
} : TSchema[K];
|
|
101
|
+
}> : TAlt extends {
|
|
102
|
+
kind: "setNotNull";
|
|
103
|
+
} ? Prettify<Omit<TSchema, K> & {
|
|
104
|
+
[P in K]: TSchema[K] extends ColumnDescriptor ? {
|
|
105
|
+
tsType: TSchema[K]["tsType"];
|
|
106
|
+
isNullable: false;
|
|
107
|
+
hasDefault: TSchema[K]["hasDefault"];
|
|
108
|
+
isAutoIncrement: TSchema[K]["isAutoIncrement"];
|
|
109
|
+
} : TSchema[K];
|
|
110
|
+
}> : TAlt extends {
|
|
111
|
+
kind: "dropNotNull";
|
|
112
|
+
} ? Prettify<Omit<TSchema, K> & {
|
|
113
|
+
[P in K]: TSchema[K] extends ColumnDescriptor ? {
|
|
114
|
+
tsType: TSchema[K]["tsType"];
|
|
115
|
+
isNullable: true;
|
|
116
|
+
hasDefault: TSchema[K]["hasDefault"];
|
|
117
|
+
isAutoIncrement: TSchema[K]["isAutoIncrement"];
|
|
118
|
+
} : TSchema[K];
|
|
119
|
+
}> : TSchema : TSchema : THeadOp extends ModifyColumnOp<infer K, any, infer TDescriptor> ? Prettify<Omit<TSchema, K> & {
|
|
120
|
+
[P in K]: TDescriptor;
|
|
71
121
|
}> : TSchema;
|
|
72
122
|
/**
|
|
73
123
|
* Recursively processes a list of alteration operations (AST)
|
|
@@ -15,4 +15,70 @@
|
|
|
15
15
|
* @param startMarker The marker in the document to start injecting the app.
|
|
16
16
|
* @param endMarker The marker in the app stream that signals the end of the initial, non-suspended render.
|
|
17
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* A utility that orchestrates and interleaves three ReadableStreams to produce a
|
|
20
|
+
* single, valid HTML response stream. It uses two special markers:
|
|
21
|
+
*
|
|
22
|
+
* - `startMarker`: Placed in the `outerHtml` stream (the document shell) to
|
|
23
|
+
* designate where the application's content should be injected.
|
|
24
|
+
* - `endMarker`: Injected into the `innerHtml` stream's RSC payload to signal
|
|
25
|
+
* the end of the initial, non-suspended render. This marker is needed for
|
|
26
|
+
* non-blocking hydration, as it allows the stitching process to send the
|
|
27
|
+
* client `<script>` tags before all suspended content has resolved.
|
|
28
|
+
*
|
|
29
|
+
* It manages three main stream readers:
|
|
30
|
+
*
|
|
31
|
+
* - `hoistedTagsReader`: Reads from the `hoistedTagsStream`, which contains only
|
|
32
|
+
* the hoisted meta tags (e.g., `<title>`, `<meta>`).
|
|
33
|
+
* - `outerReader`: Reads from the `outerHtml` stream, which is the server-rendered
|
|
34
|
+
* document shell (containing `<html>`, `<head>`, etc.).
|
|
35
|
+
* - `innerReader`: Reads from the `appBodyStream`, which contains the main
|
|
36
|
+
* application content, stripped of its hoisted tags.
|
|
37
|
+
*
|
|
38
|
+
* The function proceeds through a multi-phase state machine, managed by the
|
|
39
|
+
* `pump` function, to correctly interleave these streams.
|
|
40
|
+
*
|
|
41
|
+
* The state machine moves through the following phases:
|
|
42
|
+
*
|
|
43
|
+
* 1. `read-hoisted`:
|
|
44
|
+
* - **Goal:** Buffer all hoisted tags from the `hoistedTagsStream`.
|
|
45
|
+
* - **Action:** Reads from `hoistedTagsReader` and appends all content into
|
|
46
|
+
* the `hoistedTagsBuffer`. Does not enqueue anything yet.
|
|
47
|
+
* - **Transition:** Moves to `outer-head` when the stream is exhausted.
|
|
48
|
+
*
|
|
49
|
+
* 2. `outer-head`:
|
|
50
|
+
* - **Goal:** Stream the document up to the closing `</head>` tag, inject the
|
|
51
|
+
* hoisted tags, and then continue until the app `startMarker`.
|
|
52
|
+
* - **Action:** Reads from `outerReader`. When it finds `</head>`, it enqueues
|
|
53
|
+
* the content before it, then enqueues the `hoistedTagsBuffer`, and finally
|
|
54
|
+
* enqueues the `</head>` tag itself. It then continues reading from
|
|
55
|
+
* `outerReader` until it finds the `startMarker`.
|
|
56
|
+
* - **Transition:** Moves to `inner-shell` after finding and discarding the
|
|
57
|
+
* `startMarker`.
|
|
58
|
+
*
|
|
59
|
+
* 3. `inner-shell`:
|
|
60
|
+
* - **Goal:** Stream the initial, non-suspended part of the application.
|
|
61
|
+
* - **Action:** Switches to `innerReader`. It enqueues chunks until it finds
|
|
62
|
+
* the `endMarker`. Any content after the marker is stored in
|
|
63
|
+
* `innerSuspendedRemains`.
|
|
64
|
+
* - **Transition:** Moves to `outer-tail` after finding the `endMarker`.
|
|
65
|
+
*
|
|
66
|
+
* 4. `outer-tail`:
|
|
67
|
+
* - **Goal:** Stream the rest of the document's `<body>`, including client
|
|
68
|
+
* `<script>` tags.
|
|
69
|
+
* - **Action:** Switches back to `outerReader` and enqueues chunks until it
|
|
70
|
+
* finds the `</body>` tag.
|
|
71
|
+
* - **Transition:** Moves to `inner-suspended` after finding `</body>`.
|
|
72
|
+
*
|
|
73
|
+
* 5. `inner-suspended`:
|
|
74
|
+
* - **Goal:** Stream any suspended content from the React app.
|
|
75
|
+
* - **Action:** First enqueues any content from `innerSuspendedRemains`, then
|
|
76
|
+
* continues reading from `innerReader` until the stream is exhausted.
|
|
77
|
+
* - **Transition:** Moves to `outer-end` when the stream is exhausted.
|
|
78
|
+
*
|
|
79
|
+
* 6. `outer-end`:
|
|
80
|
+
* - **Goal:** Finish the document.
|
|
81
|
+
* - **Action:** Switches back to `outerReader` for the last time to send the
|
|
82
|
+
* closing `</body>` and `</html>` tags.
|
|
83
|
+
*/
|
|
18
84
|
export declare function stitchDocumentAndAppStreams(outerHtml: ReadableStream<Uint8Array>, innerHtml: ReadableStream<Uint8Array>, startMarker: string, endMarker: string): ReadableStream<Uint8Array>;
|
|
@@ -105,6 +105,72 @@ function splitStreamOnFirstNonHoistedTag(sourceStream) {
|
|
|
105
105
|
});
|
|
106
106
|
return [hoistedTagsStream, appBodyStream];
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* A utility that orchestrates and interleaves three ReadableStreams to produce a
|
|
110
|
+
* single, valid HTML response stream. It uses two special markers:
|
|
111
|
+
*
|
|
112
|
+
* - `startMarker`: Placed in the `outerHtml` stream (the document shell) to
|
|
113
|
+
* designate where the application's content should be injected.
|
|
114
|
+
* - `endMarker`: Injected into the `innerHtml` stream's RSC payload to signal
|
|
115
|
+
* the end of the initial, non-suspended render. This marker is needed for
|
|
116
|
+
* non-blocking hydration, as it allows the stitching process to send the
|
|
117
|
+
* client `<script>` tags before all suspended content has resolved.
|
|
118
|
+
*
|
|
119
|
+
* It manages three main stream readers:
|
|
120
|
+
*
|
|
121
|
+
* - `hoistedTagsReader`: Reads from the `hoistedTagsStream`, which contains only
|
|
122
|
+
* the hoisted meta tags (e.g., `<title>`, `<meta>`).
|
|
123
|
+
* - `outerReader`: Reads from the `outerHtml` stream, which is the server-rendered
|
|
124
|
+
* document shell (containing `<html>`, `<head>`, etc.).
|
|
125
|
+
* - `innerReader`: Reads from the `appBodyStream`, which contains the main
|
|
126
|
+
* application content, stripped of its hoisted tags.
|
|
127
|
+
*
|
|
128
|
+
* The function proceeds through a multi-phase state machine, managed by the
|
|
129
|
+
* `pump` function, to correctly interleave these streams.
|
|
130
|
+
*
|
|
131
|
+
* The state machine moves through the following phases:
|
|
132
|
+
*
|
|
133
|
+
* 1. `read-hoisted`:
|
|
134
|
+
* - **Goal:** Buffer all hoisted tags from the `hoistedTagsStream`.
|
|
135
|
+
* - **Action:** Reads from `hoistedTagsReader` and appends all content into
|
|
136
|
+
* the `hoistedTagsBuffer`. Does not enqueue anything yet.
|
|
137
|
+
* - **Transition:** Moves to `outer-head` when the stream is exhausted.
|
|
138
|
+
*
|
|
139
|
+
* 2. `outer-head`:
|
|
140
|
+
* - **Goal:** Stream the document up to the closing `</head>` tag, inject the
|
|
141
|
+
* hoisted tags, and then continue until the app `startMarker`.
|
|
142
|
+
* - **Action:** Reads from `outerReader`. When it finds `</head>`, it enqueues
|
|
143
|
+
* the content before it, then enqueues the `hoistedTagsBuffer`, and finally
|
|
144
|
+
* enqueues the `</head>` tag itself. It then continues reading from
|
|
145
|
+
* `outerReader` until it finds the `startMarker`.
|
|
146
|
+
* - **Transition:** Moves to `inner-shell` after finding and discarding the
|
|
147
|
+
* `startMarker`.
|
|
148
|
+
*
|
|
149
|
+
* 3. `inner-shell`:
|
|
150
|
+
* - **Goal:** Stream the initial, non-suspended part of the application.
|
|
151
|
+
* - **Action:** Switches to `innerReader`. It enqueues chunks until it finds
|
|
152
|
+
* the `endMarker`. Any content after the marker is stored in
|
|
153
|
+
* `innerSuspendedRemains`.
|
|
154
|
+
* - **Transition:** Moves to `outer-tail` after finding the `endMarker`.
|
|
155
|
+
*
|
|
156
|
+
* 4. `outer-tail`:
|
|
157
|
+
* - **Goal:** Stream the rest of the document's `<body>`, including client
|
|
158
|
+
* `<script>` tags.
|
|
159
|
+
* - **Action:** Switches back to `outerReader` and enqueues chunks until it
|
|
160
|
+
* finds the `</body>` tag.
|
|
161
|
+
* - **Transition:** Moves to `inner-suspended` after finding `</body>`.
|
|
162
|
+
*
|
|
163
|
+
* 5. `inner-suspended`:
|
|
164
|
+
* - **Goal:** Stream any suspended content from the React app.
|
|
165
|
+
* - **Action:** First enqueues any content from `innerSuspendedRemains`, then
|
|
166
|
+
* continues reading from `innerReader` until the stream is exhausted.
|
|
167
|
+
* - **Transition:** Moves to `outer-end` when the stream is exhausted.
|
|
168
|
+
*
|
|
169
|
+
* 6. `outer-end`:
|
|
170
|
+
* - **Goal:** Finish the document.
|
|
171
|
+
* - **Action:** Switches back to `outerReader` for the last time to send the
|
|
172
|
+
* closing `</body>` and `</html>` tags.
|
|
173
|
+
*/
|
|
108
174
|
export function stitchDocumentAndAppStreams(outerHtml, innerHtml, startMarker, endMarker) {
|
|
109
175
|
const [hoistedTagsStream, appBodyStream] = splitStreamOnFirstNonHoistedTag(innerHtml);
|
|
110
176
|
const decoder = new TextDecoder();
|
|
@@ -115,138 +181,211 @@ export function stitchDocumentAndAppStreams(outerHtml, innerHtml, startMarker, e
|
|
|
115
181
|
let buffer = "";
|
|
116
182
|
let outerBufferRemains = "";
|
|
117
183
|
let innerSuspendedRemains = "";
|
|
118
|
-
let
|
|
184
|
+
let hoistedTagsBuffer = "";
|
|
185
|
+
let hoistedTagsReady = false;
|
|
186
|
+
let phase = "read-hoisted";
|
|
119
187
|
const pump = async (controller) => {
|
|
188
|
+
const enqueue = (text) => {
|
|
189
|
+
if (text) {
|
|
190
|
+
controller.enqueue(encoder.encode(text));
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const flush = () => {
|
|
194
|
+
const flushIndex = buffer.lastIndexOf("\n");
|
|
195
|
+
if (flushIndex !== -1) {
|
|
196
|
+
enqueue(buffer.slice(0, flushIndex + 1));
|
|
197
|
+
buffer = buffer.slice(flushIndex + 1);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
120
200
|
try {
|
|
121
|
-
if (phase === "
|
|
201
|
+
if (phase === "read-hoisted") {
|
|
202
|
+
// Continuously read from the hoisted tags stream and buffer the
|
|
203
|
+
// content. Once the stream is finished, transition to the next phase.
|
|
122
204
|
const { done, value } = await hoistedTagsReader.read();
|
|
205
|
+
// When the stream is done, we're ready to process the document head.
|
|
123
206
|
if (done) {
|
|
207
|
+
hoistedTagsReady = true;
|
|
124
208
|
phase = "outer-head";
|
|
125
209
|
}
|
|
126
210
|
else {
|
|
127
|
-
|
|
211
|
+
// Otherwise, keep appending to the buffer.
|
|
212
|
+
hoistedTagsBuffer += decoder.decode(value, { stream: true });
|
|
128
213
|
}
|
|
129
214
|
}
|
|
130
215
|
else if (phase === "outer-head") {
|
|
216
|
+
// Read from the document stream. Search for the closing `</head>` tag
|
|
217
|
+
// to inject the buffered hoisted tags. Then, search for the
|
|
218
|
+
// `startMarker` to know when to start injecting the app shell. Once
|
|
219
|
+
// the marker is found, transition to the next phase.
|
|
131
220
|
const { done, value } = await outerReader.read();
|
|
221
|
+
// Handle the case where the document stream ends.
|
|
132
222
|
if (done) {
|
|
223
|
+
// If there's content left in the buffer, process it for markers.
|
|
133
224
|
if (buffer) {
|
|
225
|
+
const headCloseIndex = buffer.indexOf("</head>");
|
|
226
|
+
if (headCloseIndex !== -1 &&
|
|
227
|
+
hoistedTagsReady &&
|
|
228
|
+
hoistedTagsBuffer) {
|
|
229
|
+
enqueue(buffer.slice(0, headCloseIndex));
|
|
230
|
+
enqueue(hoistedTagsBuffer);
|
|
231
|
+
hoistedTagsBuffer = "";
|
|
232
|
+
enqueue("</head>");
|
|
233
|
+
buffer = buffer.slice(headCloseIndex + "</head>".length);
|
|
234
|
+
}
|
|
134
235
|
const markerIndex = buffer.indexOf(startMarker);
|
|
135
236
|
if (markerIndex !== -1) {
|
|
136
|
-
|
|
237
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
137
238
|
outerBufferRemains = buffer.slice(markerIndex + startMarker.length);
|
|
138
239
|
}
|
|
139
240
|
else {
|
|
140
|
-
|
|
241
|
+
enqueue(buffer);
|
|
141
242
|
}
|
|
142
243
|
buffer = "";
|
|
143
244
|
}
|
|
245
|
+
else if (hoistedTagsReady && hoistedTagsBuffer) {
|
|
246
|
+
enqueue(hoistedTagsBuffer);
|
|
247
|
+
hoistedTagsBuffer = "";
|
|
248
|
+
}
|
|
249
|
+
// Even if the stream ends, we must proceed to the app shell phase.
|
|
144
250
|
phase = "inner-shell";
|
|
145
251
|
}
|
|
146
252
|
else {
|
|
253
|
+
// As chunks arrive, append them to the buffer.
|
|
147
254
|
buffer += decoder.decode(value, { stream: true });
|
|
255
|
+
// Search for the closing head tag to inject hoisted tags.
|
|
256
|
+
const headCloseIndex = buffer.indexOf("</head>");
|
|
257
|
+
if (headCloseIndex !== -1 && hoistedTagsReady && hoistedTagsBuffer) {
|
|
258
|
+
enqueue(buffer.slice(0, headCloseIndex));
|
|
259
|
+
enqueue(hoistedTagsBuffer);
|
|
260
|
+
hoistedTagsBuffer = "";
|
|
261
|
+
enqueue("</head>");
|
|
262
|
+
buffer = buffer.slice(headCloseIndex + "</head>".length);
|
|
263
|
+
}
|
|
264
|
+
// Search for the start marker to switch to the app stream.
|
|
148
265
|
const markerIndex = buffer.indexOf(startMarker);
|
|
149
266
|
if (markerIndex !== -1) {
|
|
150
|
-
|
|
267
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
151
268
|
outerBufferRemains = buffer.slice(markerIndex + startMarker.length);
|
|
152
269
|
buffer = "";
|
|
153
270
|
phase = "inner-shell";
|
|
154
271
|
}
|
|
155
272
|
else {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
buffer = buffer.slice(flushIndex + 1);
|
|
160
|
-
}
|
|
273
|
+
// If no marker is found yet, flush the buffer up to the last
|
|
274
|
+
// newline to keep the stream flowing.
|
|
275
|
+
flush();
|
|
161
276
|
}
|
|
162
277
|
}
|
|
163
278
|
}
|
|
164
279
|
else if (phase === "inner-shell") {
|
|
280
|
+
// Now read from the app stream. We send the initial part of the app
|
|
281
|
+
// content until we find the `endMarker`. This marker tells us that the
|
|
282
|
+
// non-suspended part of the app is rendered. Any content after this
|
|
283
|
+
// marker is considered suspended and is buffered. Then, transition.
|
|
165
284
|
const { done, value } = await innerReader.read();
|
|
285
|
+
// Handle the case where the app stream ends.
|
|
166
286
|
if (done) {
|
|
167
287
|
if (buffer)
|
|
168
|
-
|
|
288
|
+
enqueue(buffer);
|
|
169
289
|
phase = "outer-tail";
|
|
170
290
|
}
|
|
171
291
|
else {
|
|
292
|
+
// As chunks arrive, append them to the buffer.
|
|
172
293
|
buffer += decoder.decode(value, { stream: true });
|
|
173
294
|
const markerIndex = buffer.indexOf(endMarker);
|
|
295
|
+
// If the end marker is found, enqueue content up to the marker,
|
|
296
|
+
// buffer the rest, and switch to the document tail phase.
|
|
174
297
|
if (markerIndex !== -1) {
|
|
175
298
|
const endOfMarkerIndex = markerIndex + endMarker.length;
|
|
176
|
-
|
|
299
|
+
enqueue(buffer.slice(0, endOfMarkerIndex));
|
|
177
300
|
innerSuspendedRemains = buffer.slice(endOfMarkerIndex);
|
|
178
301
|
buffer = "";
|
|
179
302
|
phase = "outer-tail";
|
|
180
303
|
}
|
|
181
304
|
else {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
|
|
185
|
-
buffer = buffer.slice(flushIndex + 1);
|
|
186
|
-
}
|
|
305
|
+
// If no marker is found yet, flush the buffer.
|
|
306
|
+
flush();
|
|
187
307
|
}
|
|
188
308
|
}
|
|
189
309
|
}
|
|
190
310
|
else if (phase === "outer-tail") {
|
|
311
|
+
// Switch back to the document stream. The goal is to send the rest of
|
|
312
|
+
// the document's body, which critically includes the client-side
|
|
313
|
+
// `<script>` tags for hydration. We stream until we find the closing
|
|
314
|
+
// `</body>` tag and then transition.
|
|
315
|
+
// First, process any leftover buffer from the `outer-head` phase.
|
|
191
316
|
if (outerBufferRemains) {
|
|
192
317
|
buffer = outerBufferRemains;
|
|
193
318
|
outerBufferRemains = "";
|
|
194
319
|
}
|
|
195
320
|
const { done, value } = await outerReader.read();
|
|
321
|
+
// Handle the case where the document stream ends.
|
|
196
322
|
if (done) {
|
|
197
323
|
if (buffer) {
|
|
324
|
+
// Search the remaining buffer for the closing body tag.
|
|
198
325
|
const markerIndex = buffer.indexOf("</body>");
|
|
199
326
|
if (markerIndex !== -1) {
|
|
200
|
-
|
|
327
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
201
328
|
buffer = buffer.slice(markerIndex);
|
|
202
329
|
}
|
|
203
330
|
else {
|
|
204
|
-
|
|
331
|
+
enqueue(buffer);
|
|
205
332
|
buffer = "";
|
|
206
333
|
}
|
|
207
334
|
}
|
|
335
|
+
// Proceed to the suspended content phase.
|
|
208
336
|
phase = "inner-suspended";
|
|
209
337
|
}
|
|
210
338
|
else {
|
|
339
|
+
// As chunks arrive, append them to the buffer.
|
|
211
340
|
buffer += decoder.decode(value, { stream: true });
|
|
341
|
+
// Search for the closing body tag to switch to suspended content.
|
|
212
342
|
const markerIndex = buffer.indexOf("</body>");
|
|
213
343
|
if (markerIndex !== -1) {
|
|
214
|
-
|
|
344
|
+
enqueue(buffer.slice(0, markerIndex));
|
|
215
345
|
buffer = buffer.slice(markerIndex);
|
|
216
346
|
phase = "inner-suspended";
|
|
217
347
|
}
|
|
218
348
|
else {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
controller.enqueue(encoder.encode(buffer.slice(0, flushIndex + 1)));
|
|
222
|
-
buffer = buffer.slice(flushIndex + 1);
|
|
223
|
-
}
|
|
349
|
+
// If no marker is found yet, flush the buffer.
|
|
350
|
+
flush();
|
|
224
351
|
}
|
|
225
352
|
}
|
|
226
353
|
}
|
|
227
354
|
else if (phase === "inner-suspended") {
|
|
355
|
+
// Switch back to the app stream. First, send any buffered suspended
|
|
356
|
+
// content from the `inner-shell` phase. Then, stream the rest of the
|
|
357
|
+
// app content until it's finished. This is all the content that was
|
|
358
|
+
// behind a `<Suspense>` boundary.
|
|
359
|
+
// First, send any buffered suspended content from the `inner-shell` phase.
|
|
228
360
|
if (innerSuspendedRemains) {
|
|
229
|
-
|
|
361
|
+
enqueue(innerSuspendedRemains);
|
|
230
362
|
innerSuspendedRemains = "";
|
|
231
363
|
}
|
|
232
364
|
const { done, value } = await innerReader.read();
|
|
365
|
+
// When the app stream is done, transition to the final phase.
|
|
233
366
|
if (done) {
|
|
234
367
|
phase = "outer-end";
|
|
235
368
|
}
|
|
236
369
|
else {
|
|
370
|
+
// Otherwise, pass through the remaining app content directly.
|
|
237
371
|
controller.enqueue(value);
|
|
238
372
|
}
|
|
239
373
|
}
|
|
240
374
|
else if (phase === "outer-end") {
|
|
375
|
+
// Finally, switch back to the document stream one last time to send
|
|
376
|
+
// the closing `</body>` and `</html>` tags and finish the response.
|
|
377
|
+
// First, send any leftover buffer from the `outer-tail` phase.
|
|
241
378
|
if (buffer) {
|
|
242
|
-
|
|
379
|
+
enqueue(buffer);
|
|
243
380
|
buffer = "";
|
|
244
381
|
}
|
|
245
382
|
const { done, value } = await outerReader.read();
|
|
383
|
+
// When the document stream is done, we're finished.
|
|
246
384
|
if (done) {
|
|
247
385
|
controller.close();
|
|
248
386
|
return;
|
|
249
387
|
}
|
|
388
|
+
// Otherwise, pass through the final document content.
|
|
250
389
|
controller.enqueue(value);
|
|
251
390
|
}
|
|
252
391
|
await pump(controller);
|
|
@@ -45,7 +45,7 @@ describe("stitchDocumentAndAppStreams", () => {
|
|
|
45
45
|
const startMarker = '<div id="rwsdk-app-start" />';
|
|
46
46
|
const endMarker = '<div id="rwsdk-app-end"></div>';
|
|
47
47
|
describe("meta tag hoisting", () => {
|
|
48
|
-
it("
|
|
48
|
+
it("places hoisted tags inside head, after existing head content", async () => {
|
|
49
49
|
const outerHtml = `<!DOCTYPE html>
|
|
50
50
|
<html>
|
|
51
51
|
<head>
|
|
@@ -59,10 +59,19 @@ describe("stitchDocumentAndAppStreams", () => {
|
|
|
59
59
|
const innerHtml = `<title>Page Title</title><div>App content</div>${endMarker}`;
|
|
60
60
|
const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker));
|
|
61
61
|
expect(result).toContain(`<title>Page Title</title>`);
|
|
62
|
-
expect(result.
|
|
62
|
+
expect(result).toMatch(/<head>[\s\S]*<meta charset="utf-8" \/>[\s\S]*<title>Page Title<\/title>[\s\S]*<\/head>/);
|
|
63
63
|
expect(result).toContain(`<div>App content</div>`);
|
|
64
|
+
const doctypeIndex = result.indexOf(`<!DOCTYPE html>`);
|
|
65
|
+
const headIndex = result.indexOf(`<head>`);
|
|
66
|
+
const charsetIndex = result.indexOf(`<meta charset="utf-8" />`);
|
|
67
|
+
const titleIndex = result.indexOf(`<title>Page Title</title>`);
|
|
68
|
+
const headCloseIndex = result.indexOf(`</head>`);
|
|
69
|
+
expect(doctypeIndex).toBe(0);
|
|
70
|
+
expect(doctypeIndex).toBeLessThan(headIndex);
|
|
71
|
+
expect(charsetIndex).toBeLessThan(titleIndex);
|
|
72
|
+
expect(titleIndex).toBeLessThan(headCloseIndex);
|
|
64
73
|
});
|
|
65
|
-
it("
|
|
74
|
+
it("places multiple hoisted tags inside head, after existing head content", async () => {
|
|
66
75
|
const outerHtml = `<!DOCTYPE html>
|
|
67
76
|
<html>
|
|
68
77
|
<head>
|
|
@@ -78,9 +87,14 @@ describe("stitchDocumentAndAppStreams", () => {
|
|
|
78
87
|
expect(result).toContain(`<title>Page Title</title>`);
|
|
79
88
|
expect(result).toContain(`<meta name="description" content="Test" />`);
|
|
80
89
|
expect(result).toContain(`<link rel="stylesheet" href="/styles.css" />`);
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
90
|
+
expect(result).toMatch(/<head>[\s\S]*<meta charset="utf-8" \/>[\s\S]*<title>Page Title<\/title>[\s\S]*<meta name="description" content="Test" \/>[\s\S]*<link rel="stylesheet" href="\/styles.css" \/>[\s\S]*<\/head>/);
|
|
91
|
+
const doctypeIndex = result.indexOf(`<!DOCTYPE html>`);
|
|
92
|
+
const charsetIndex = result.indexOf(`<meta charset="utf-8" />`);
|
|
93
|
+
const titleIndex = result.indexOf(`<title>Page Title</title>`);
|
|
94
|
+
const headCloseIndex = result.indexOf(`</head>`);
|
|
95
|
+
expect(doctypeIndex).toBe(0);
|
|
96
|
+
expect(charsetIndex).toBeLessThan(titleIndex);
|
|
97
|
+
expect(titleIndex).toBeLessThan(headCloseIndex);
|
|
84
98
|
});
|
|
85
99
|
it("handles app stream with no hoisted tags", async () => {
|
|
86
100
|
const outerHtml = `<!DOCTYPE html>
|
|
@@ -117,9 +131,29 @@ describe("stitchDocumentAndAppStreams", () => {
|
|
|
117
131
|
const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), createChunkedStream(innerHtmlChunks), startMarker, endMarker));
|
|
118
132
|
expect(result).toContain(`<title>Page Title</title>`);
|
|
119
133
|
expect(result).toContain(`<meta name="description" content="Test" />`);
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
134
|
+
expect(result).toMatch(/<head>[\s\S]*<meta charset="utf-8" \/>[\s\S]*<title>Page Title<\/title>[\s\S]*<\/head>/);
|
|
135
|
+
const doctypeIndex = result.indexOf(`<!DOCTYPE html>`);
|
|
136
|
+
const charsetIndex = result.indexOf(`<meta charset="utf-8" />`);
|
|
137
|
+
const titleIndex = result.indexOf(`<title>Page Title</title>`);
|
|
138
|
+
const headCloseIndex = result.indexOf(`</head>`);
|
|
139
|
+
expect(doctypeIndex).toBe(0);
|
|
140
|
+
expect(charsetIndex).toBeLessThan(titleIndex);
|
|
141
|
+
expect(titleIndex).toBeLessThan(headCloseIndex);
|
|
142
|
+
});
|
|
143
|
+
it("ensures doctype is always first", async () => {
|
|
144
|
+
const outerHtml = `<!DOCTYPE html>
|
|
145
|
+
<html>
|
|
146
|
+
<head>
|
|
147
|
+
<meta charset="utf-8" />
|
|
148
|
+
</head>
|
|
149
|
+
<body>
|
|
150
|
+
${startMarker}
|
|
151
|
+
<script src="/client.js"></script>
|
|
152
|
+
</body>
|
|
153
|
+
</html>`;
|
|
154
|
+
const innerHtml = `<title>Page Title</title><div>App content</div>${endMarker}`;
|
|
155
|
+
const result = await streamToString(stitchDocumentAndAppStreams(stringToStream(outerHtml), stringToStream(innerHtml), startMarker, endMarker));
|
|
156
|
+
expect(result.trim().startsWith(`<!DOCTYPE html>`)).toBe(true);
|
|
123
157
|
});
|
|
124
158
|
});
|
|
125
159
|
describe("basic stitching flow", () => {
|