create-monolayer-app 0.1.2 → 0.2.1
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/package.json +1 -1
- package/template/_gitignore +5 -0
- package/template/lib/db/client.ts +30 -0
- package/template/lib/db/db.ts +4 -0
- package/template/oxlint.config.ts +11 -0
- package/template/package.json +23 -3
- package/template/tsconfig.json +3 -2
- package/template/vendor/kysely-dsql/dsql/adapter.ts +15 -0
- package/template/vendor/kysely-dsql/dsql/alter-column-builder.ts +105 -0
- package/template/vendor/kysely-dsql/dsql/alter-table-builders.ts +195 -0
- package/template/vendor/kysely-dsql/dsql/compatibility-rules.ts +235 -0
- package/template/vendor/kysely-dsql/dsql/create-column-builder.ts +187 -0
- package/template/vendor/kysely-dsql/dsql/create-table-builder.ts +115 -0
- package/template/vendor/kysely-dsql/dsql/dialect.ts +41 -0
- package/template/vendor/kysely-dsql/dsql/dsql-kysely.ts +93 -0
- package/template/vendor/kysely-dsql/dsql/dsql-token.ts +29 -0
- package/template/vendor/kysely-dsql/dsql/index-view-builders.ts +206 -0
- package/template/vendor/kysely-dsql/dsql/pool.ts +40 -0
- package/template/vendor/kysely-dsql/dsql/query-compiler-error.ts +18 -0
- package/template/vendor/kysely-dsql/dsql/query-compiler.test.ts +765 -0
- package/template/vendor/kysely-dsql/dsql/query-compiler.ts +259 -0
- package/template/vendor/kysely-dsql/dsql/query-validation.test.ts +518 -0
- package/template/vendor/kysely-dsql/dsql/query-validation.ts +128 -0
- package/template/vendor/kysely-dsql/dsql/raw-statement.test.ts +167 -0
- package/template/vendor/kysely-dsql/dsql/raw-statement.ts +51 -0
- package/template/vendor/kysely-dsql/dsql/schema-module.ts +167 -0
- package/template/vendor/kysely-dsql/dsql/types.ts +4 -0
- package/template/vendor/kysely-dsql/generator/create-migration.ts +86 -0
- package/template/vendor/kysely-dsql/generator/generate-types-from-pglite.ts +40 -0
- package/template/vendor/kysely-dsql/generator/generated-types-writer.ts +73 -0
- package/template/vendor/kysely-dsql/generator/migration-file.ts +73 -0
- package/template/vendor/kysely-dsql/generator/type-generator.ts +330 -0
- package/template/vendor/kysely-dsql/plugins/auto-updated-at.plugin.ts +139 -0
- package/template/vendor/kysely-dsql/plugins/index.ts +10 -0
- package/template/vendor/kysely-dsql/plugins/soft-delete.plugin.ts +217 -0
- package/template/vitest.config.ts +23 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// oxlint-disable max-classes-per-file typescript/no-explicit-any
|
|
2
|
+
import { Kysely, PostgresIntrospector, sql } from "kysely";
|
|
3
|
+
import type {
|
|
4
|
+
DatabaseConnection,
|
|
5
|
+
Dialect,
|
|
6
|
+
DialectAdapter,
|
|
7
|
+
Driver,
|
|
8
|
+
RawBuilder,
|
|
9
|
+
TransactionSettings,
|
|
10
|
+
} from "kysely";
|
|
11
|
+
import {
|
|
12
|
+
afterAll,
|
|
13
|
+
beforeAll,
|
|
14
|
+
describe,
|
|
15
|
+
expect,
|
|
16
|
+
expectTypeOf,
|
|
17
|
+
it,
|
|
18
|
+
vi,
|
|
19
|
+
} from "vitest";
|
|
20
|
+
|
|
21
|
+
import { DQSLAdapter } from "./adapter.js";
|
|
22
|
+
import { DSQLQueryCompilerError } from "./query-compiler-error.js";
|
|
23
|
+
import { DSQLQueryCompiler } from "./query-compiler.js";
|
|
24
|
+
import { createDSQLRawStatement } from "./raw-statement.js";
|
|
25
|
+
import type { DSQLRawStatementResult } from "./raw-statement.js";
|
|
26
|
+
|
|
27
|
+
class SpyDriver implements Driver {
|
|
28
|
+
readonly acquireConnection = vi.fn<() => Promise<DatabaseConnection>>();
|
|
29
|
+
readonly releaseConnection = vi.fn<() => Promise<void>>().mockResolvedValue();
|
|
30
|
+
readonly init = vi.fn<() => Promise<void>>().mockResolvedValue();
|
|
31
|
+
readonly destroy = vi.fn<() => Promise<void>>().mockResolvedValue();
|
|
32
|
+
readonly beginTransaction = vi
|
|
33
|
+
.fn<
|
|
34
|
+
(
|
|
35
|
+
connection: DatabaseConnection,
|
|
36
|
+
settings: TransactionSettings
|
|
37
|
+
) => Promise<void>
|
|
38
|
+
>()
|
|
39
|
+
.mockResolvedValue();
|
|
40
|
+
readonly commitTransaction = vi
|
|
41
|
+
.fn<(connection: DatabaseConnection) => Promise<void>>()
|
|
42
|
+
.mockResolvedValue();
|
|
43
|
+
readonly rollbackTransaction = vi
|
|
44
|
+
.fn<(connection: DatabaseConnection) => Promise<void>>()
|
|
45
|
+
.mockResolvedValue();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class SpyDialect implements Dialect {
|
|
49
|
+
readonly driver: SpyDriver;
|
|
50
|
+
|
|
51
|
+
constructor(driver: SpyDriver) {
|
|
52
|
+
this.driver = driver;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
createDriver(): Driver {
|
|
56
|
+
return this.driver;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// oxlint-disable-next-line class-methods-use-this
|
|
60
|
+
createQueryCompiler(): DSQLQueryCompiler {
|
|
61
|
+
return new DSQLQueryCompiler();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// oxlint-disable-next-line class-methods-use-this
|
|
65
|
+
createAdapter(): DialectAdapter {
|
|
66
|
+
return new DQSLAdapter();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// oxlint-disable-next-line class-methods-use-this
|
|
70
|
+
createIntrospector(database: Kysely<any>): PostgresIntrospector {
|
|
71
|
+
return new PostgresIntrospector(database);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const createTestDb = (driver: SpyDriver): Kysely<any> =>
|
|
76
|
+
new Kysely<any>({
|
|
77
|
+
dialect: new SpyDialect(driver),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
interface UserRow {
|
|
81
|
+
readonly id: string;
|
|
82
|
+
readonly email: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe(createDSQLRawStatement, () => {
|
|
86
|
+
beforeAll(() => {
|
|
87
|
+
vi.stubEnv("CLUSTER_ENDPOINT", "example.dsql.us-east-1.on.aws");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterAll(() => {
|
|
91
|
+
vi.unstubAllEnvs();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns ok: true and a compilable RawBuilder for supported SQL", () => {
|
|
95
|
+
const driver = new SpyDriver();
|
|
96
|
+
const testDb = createTestDb(driver);
|
|
97
|
+
const sqlString = "SELECT id, email FROM users WHERE id = $1";
|
|
98
|
+
|
|
99
|
+
const result = createDSQLRawStatement<UserRow>(sqlString);
|
|
100
|
+
|
|
101
|
+
expect(result.ok).toBeTruthy();
|
|
102
|
+
// SAFETY: result.ok is verified truthy above.
|
|
103
|
+
const okResult = result as Extract<
|
|
104
|
+
DSQLRawStatementResult<UserRow>,
|
|
105
|
+
{ ok: true }
|
|
106
|
+
>;
|
|
107
|
+
const compiled = okResult.query.compile(testDb);
|
|
108
|
+
expect(compiled.sql).toBe(sqlString);
|
|
109
|
+
expectTypeOf(okResult.query).toEqualTypeOf<RawBuilder<UserRow>>();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns ok: false with validation issues for unsupported SQL", () => {
|
|
113
|
+
const result = createDSQLRawStatement("TRUNCATE TABLE users;");
|
|
114
|
+
|
|
115
|
+
expect(result.ok).toBeFalsy();
|
|
116
|
+
expect("query" in result).toBeFalsy();
|
|
117
|
+
// SAFETY: result.ok is verified falsy above.
|
|
118
|
+
const errResult = result as Extract<
|
|
119
|
+
DSQLRawStatementResult<unknown>,
|
|
120
|
+
{ ok: false }
|
|
121
|
+
>;
|
|
122
|
+
expect(errResult.issues).toStrictEqual([
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
code: "truncate",
|
|
125
|
+
feature: "Table truncation",
|
|
126
|
+
documentationUrl: expect.stringMatching(
|
|
127
|
+
/^https:\/\/docs\.aws\.amazon\.com\/aurora-dsql\//u
|
|
128
|
+
),
|
|
129
|
+
}),
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("does not throw for invalid SQL input", () => {
|
|
134
|
+
expect(() => {
|
|
135
|
+
createDSQLRawStatement(
|
|
136
|
+
"ALTER TABLE users ADD CONSTRAINT fk FOREIGN KEY (dept_id) REFERENCES dept(id)"
|
|
137
|
+
);
|
|
138
|
+
}).not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("propagates generic type T to RawBuilder<T>", () => {
|
|
142
|
+
const result = createDSQLRawStatement<{ count: number }>(
|
|
143
|
+
"SELECT count(*) FROM users"
|
|
144
|
+
);
|
|
145
|
+
expectTypeOf(result).toEqualTypeOf<
|
|
146
|
+
DSQLRawStatementResult<{ count: number }>
|
|
147
|
+
>();
|
|
148
|
+
// SAFETY: Type assertion used to test generic propagation on the truthy branch.
|
|
149
|
+
const okResult = result as Extract<
|
|
150
|
+
DSQLRawStatementResult<{ count: number }>,
|
|
151
|
+
{ ok: true }
|
|
152
|
+
>;
|
|
153
|
+
expectTypeOf(okResult.query).toEqualTypeOf<RawBuilder<{ count: number }>>();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("leaves direct sql.raw protected by DSQLQueryCompiler backstop", async () => {
|
|
157
|
+
const driver = new SpyDriver();
|
|
158
|
+
const testDb = createTestDb(driver);
|
|
159
|
+
|
|
160
|
+
// Direct sql.raw bypasses createDSQLRawStatement, but the compiler catches it.
|
|
161
|
+
await expect(
|
|
162
|
+
sql.raw("DROP MATERIALIZED VIEW active_users").execute(testDb)
|
|
163
|
+
).rejects.toThrow(DSQLQueryCompilerError);
|
|
164
|
+
|
|
165
|
+
expect(driver.acquireConnection).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
import type { RawBuilder } from "kysely";
|
|
3
|
+
|
|
4
|
+
import { validateDSQLStatement } from "./query-validation.js";
|
|
5
|
+
import type { DSQLValidationIssue } from "./query-validation.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Result of creating a raw Aurora DSQL statement.
|
|
9
|
+
*
|
|
10
|
+
* @template T - The result row type for the raw query.
|
|
11
|
+
*/
|
|
12
|
+
export type DSQLRawStatementResult<T> =
|
|
13
|
+
| { readonly ok: true; readonly query: RawBuilder<T> }
|
|
14
|
+
| { readonly ok: false; readonly issues: readonly DSQLValidationIssue[] };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a validated raw SQL statement for Amazon Aurora DSQL.
|
|
18
|
+
*
|
|
19
|
+
* Do not pass untrusted input to this function. Use Kysely parameters for dynamic query values.
|
|
20
|
+
*
|
|
21
|
+
* This function validates the SQL string against Aurora DSQL compatibility rules.
|
|
22
|
+
* It returns a raw query builder when the SQL is valid.
|
|
23
|
+
* It returns validation issues without throwing an error when the SQL is invalid.
|
|
24
|
+
*
|
|
25
|
+
* @template T - The expected query result row type.
|
|
26
|
+
* @param statement - The raw SQL statement string to validate and build.
|
|
27
|
+
* @returns A result object containing either the raw query builder or validation issues.
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const result = createDSQLRawStatement<Person>("SELECT id, name FROM person");
|
|
31
|
+
* if (result.ok) {
|
|
32
|
+
* const query = result.query;
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createDSQLRawStatement<T = unknown>(
|
|
37
|
+
statement: string
|
|
38
|
+
): DSQLRawStatementResult<T> {
|
|
39
|
+
const validation = validateDSQLStatement(statement);
|
|
40
|
+
if (!validation.supported) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
issues: validation.issues,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
query: sql.raw<T>(statement),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { SchemaModule } from "kysely";
|
|
2
|
+
import type { KyselyPlugin } from "kysely";
|
|
3
|
+
|
|
4
|
+
import type { DSQLAlterTableBuilder } from "./alter-table-builders.js";
|
|
5
|
+
import type { DSQLCreateTableBuilder } from "./create-table-builder.js";
|
|
6
|
+
import type {
|
|
7
|
+
DSQLCreateIndexBuilder,
|
|
8
|
+
DSQLCreateViewBuilder,
|
|
9
|
+
DSQLDropIndexBuilder,
|
|
10
|
+
DSQLDropTableBuilder,
|
|
11
|
+
} from "./index-view-builders.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Supplies Kysely schema builders with the Aurora DSQL column type contract.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const schema = db.schema;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
// eslint-disable-next-line max-classes-per-file
|
|
22
|
+
export class DSQLSchemaModule extends SchemaModule {
|
|
23
|
+
/**
|
|
24
|
+
* Adds a plugin and keeps the restricted schema module.
|
|
25
|
+
*
|
|
26
|
+
* @param plugin - The Kysely plugin.
|
|
27
|
+
* @returns A restricted schema module with the plugin.
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const schema = db.schema.withPlugin(plugin);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
// @ts-expect-error DSQLSchemaModule restricts schema builders for Aurora DSQL.
|
|
34
|
+
override withPlugin(plugin: KyselyPlugin): DSQLSchemaModule {
|
|
35
|
+
// SAFETY: The subclass changes types only and adds no runtime state.
|
|
36
|
+
return super.withPlugin(plugin) as DSQLSchemaModule;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Removes all plugins and keeps the restricted schema module.
|
|
41
|
+
*
|
|
42
|
+
* @returns A restricted schema module without plugins.
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const schema = db.schema.withoutPlugins();
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
// @ts-expect-error DSQLSchemaModule restricts schema builders for Aurora DSQL.
|
|
49
|
+
override withoutPlugins(): DSQLSchemaModule {
|
|
50
|
+
// SAFETY: The subclass changes types only and adds no runtime state.
|
|
51
|
+
return super.withoutPlugins() as DSQLSchemaModule;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Selects a schema and keeps the restricted schema module.
|
|
56
|
+
*
|
|
57
|
+
* @param schema - The schema name.
|
|
58
|
+
* @returns A restricted schema module for the schema.
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const schema = db.schema.withSchema("public");
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
// @ts-expect-error DSQLSchemaModule restricts schema builders for Aurora DSQL.
|
|
65
|
+
override withSchema(schema: string): DSQLSchemaModule {
|
|
66
|
+
// SAFETY: The subclass changes types only and adds no runtime state.
|
|
67
|
+
return super.withSchema(schema) as DSQLSchemaModule;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a restricted builder for a new table.
|
|
72
|
+
*
|
|
73
|
+
* @param table - The table name.
|
|
74
|
+
* @returns A builder that accepts Aurora DSQL column types.
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* db.schema.createTable("person").addColumn("id", "uuid");
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
override createTable<TB extends string>(
|
|
81
|
+
table: TB
|
|
82
|
+
): DSQLCreateTableBuilder<TB, never> {
|
|
83
|
+
// SAFETY: The subclass changes types only and adds no runtime state.
|
|
84
|
+
return super.createTable(table) as DSQLCreateTableBuilder<TB, never>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a restricted builder for a table alteration.
|
|
89
|
+
*
|
|
90
|
+
* @param table - The table name.
|
|
91
|
+
* @returns A builder that accepts Aurora DSQL types for new columns.
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* db.schema.alterTable("person").addColumn("name", "text");
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
override alterTable(table: string): DSQLAlterTableBuilder {
|
|
98
|
+
// SAFETY: The subclass changes types only and adds no runtime state.
|
|
99
|
+
return super.alterTable(table) as DSQLAlterTableBuilder;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates a restricted builder for a new index.
|
|
104
|
+
*
|
|
105
|
+
* @param indexName - The index name.
|
|
106
|
+
* @returns A restricted index builder.
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* db.schema.createIndex("person_email_idx").on("person").column("email");
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
// @ts-expect-error DSQLSchemaModule restricts the CreateIndexBuilder surface for Aurora DSQL.
|
|
113
|
+
override createIndex(indexName: string): DSQLCreateIndexBuilder<never> {
|
|
114
|
+
// SAFETY: The subclass changes types only and adds no runtime state.
|
|
115
|
+
return super.createIndex(indexName) as DSQLCreateIndexBuilder<never>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Creates a restricted builder for a new view.
|
|
120
|
+
*
|
|
121
|
+
* @param viewName - The view name.
|
|
122
|
+
* @returns A restricted view builder.
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* db.schema.createView("active_users").as(db.selectFrom("users").selectAll());
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
override createView(viewName: string): DSQLCreateViewBuilder {
|
|
129
|
+
// SAFETY: The subclass changes types only and adds no runtime state.
|
|
130
|
+
return super.createView(viewName) as DSQLCreateViewBuilder;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Creates a restricted builder to drop an index.
|
|
135
|
+
*
|
|
136
|
+
* @param indexName - The index name.
|
|
137
|
+
* @returns A restricted drop index builder.
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* db.schema.dropIndex("person_email_idx");
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
override dropIndex(indexName: string): DSQLDropIndexBuilder {
|
|
144
|
+
// SAFETY: The subclass changes types only and adds no runtime state.
|
|
145
|
+
return super.dropIndex(indexName) as DSQLDropIndexBuilder;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Creates a restricted builder to drop a table.
|
|
150
|
+
*
|
|
151
|
+
* @param table - The table name.
|
|
152
|
+
* @returns A restricted drop table builder.
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* db.schema.dropTable("person");
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
override dropTable(table: string): DSQLDropTableBuilder {
|
|
159
|
+
// SAFETY: The subclass changes types only and adds no runtime state.
|
|
160
|
+
return super.dropTable(table) as DSQLDropTableBuilder;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
declare createType: (this: never, ...args: never[]) => never;
|
|
164
|
+
declare alterType: (this: never, ...args: never[]) => never;
|
|
165
|
+
declare dropType: (this: never, ...args: never[]) => never;
|
|
166
|
+
declare refreshMaterializedView: (this: never, ...args: never[]) => never;
|
|
167
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { createMigrationFile } from "./migration-file.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Supplies the runtime services for the migration command.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const runtime: CreateMigrationRuntime = {
|
|
12
|
+
* argv: ["node", "create-migration.js", "add-person-status"],
|
|
13
|
+
* cwd: () => process.cwd(),
|
|
14
|
+
* error: console.error,
|
|
15
|
+
* exitCode: undefined,
|
|
16
|
+
* log: console.log,
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export interface CreateMigrationRuntime {
|
|
21
|
+
/** The command-line arguments. */
|
|
22
|
+
readonly argv: readonly string[];
|
|
23
|
+
/** Gets the current working directory. */
|
|
24
|
+
readonly cwd: () => string;
|
|
25
|
+
/** Writes one error message. */
|
|
26
|
+
readonly error: (message: string) => void;
|
|
27
|
+
/** Contains the requested process exit code. */
|
|
28
|
+
exitCode: number | undefined;
|
|
29
|
+
/** Writes one success message. */
|
|
30
|
+
readonly log: (message: string) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const processRuntime: CreateMigrationRuntime = {
|
|
34
|
+
argv: process.argv,
|
|
35
|
+
cwd: process.cwd,
|
|
36
|
+
error: console.error,
|
|
37
|
+
exitCode: undefined,
|
|
38
|
+
log: console.log,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const errorMessage = (error: Error): string => error.message;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a typed migration from command-line arguments.
|
|
45
|
+
*
|
|
46
|
+
* @param runtime - The command runtime.
|
|
47
|
+
* @returns A promise that resolves after the command completes.
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* await runCreateMigration({
|
|
51
|
+
* argv: ["node", "create-migration.js", "add-person-status"],
|
|
52
|
+
* cwd: () => process.cwd(),
|
|
53
|
+
* error: console.error,
|
|
54
|
+
* exitCode: undefined,
|
|
55
|
+
* log: console.log,
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export async function runCreateMigration(
|
|
60
|
+
runtime: CreateMigrationRuntime
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const name = runtime.argv[2]?.trim() ?? "";
|
|
63
|
+
if (!name) {
|
|
64
|
+
runtime.error("Migration name is required.");
|
|
65
|
+
runtime.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const filePath = await createMigrationFile({
|
|
71
|
+
migrationDirectory: path.resolve(runtime.cwd(), "lib/db/migrations"),
|
|
72
|
+
name,
|
|
73
|
+
});
|
|
74
|
+
runtime.log(filePath);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// SAFETY: The validation and Node file operations reject with Error objects.
|
|
77
|
+
runtime.error(errorMessage(error as Error));
|
|
78
|
+
runtime.exitCode = 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const [, entryPath] = process.argv;
|
|
83
|
+
if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
|
|
84
|
+
await runCreateMigration(processRuntime);
|
|
85
|
+
process.exitCode = processRuntime.exitCode;
|
|
86
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
5
|
+
import { PGliteDialect } from "kysely";
|
|
6
|
+
|
|
7
|
+
import type { Database } from "../../../lib/db/db";
|
|
8
|
+
import { DSQLKysely } from "../dsql/dsql-kysely";
|
|
9
|
+
import plugins from "../plugins";
|
|
10
|
+
import { writeGeneratedTypes } from "./generated-types-writer.js";
|
|
11
|
+
import { generateTypes } from "./type-generator.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_OUTPUT_DIRECTORY = "lib/db";
|
|
14
|
+
|
|
15
|
+
const db: DSQLKysely<Database> = new DSQLKysely<Database>({
|
|
16
|
+
dialect: new PGliteDialect({
|
|
17
|
+
pglite: new PGlite("./.pglite_development"),
|
|
18
|
+
}),
|
|
19
|
+
plugins,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const createDatabaseTypes = async (outputDirectory: string): Promise<void> => {
|
|
23
|
+
try {
|
|
24
|
+
const types = await generateTypes(db);
|
|
25
|
+
await writeGeneratedTypes(outputDirectory, types);
|
|
26
|
+
console.log(`Database types written to ${outputDirectory}`);
|
|
27
|
+
} finally {
|
|
28
|
+
await db.destroy();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const main = async (): Promise<void> => {
|
|
33
|
+
const outputDirectory = path.resolve(
|
|
34
|
+
process.argv[2] ?? DEFAULT_OUTPUT_DIRECTORY
|
|
35
|
+
);
|
|
36
|
+
await createDatabaseTypes(outputDirectory);
|
|
37
|
+
};
|
|
38
|
+
if (!["production", "test"].includes(process.env.NODE_ENV ?? "")) {
|
|
39
|
+
await main();
|
|
40
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { GeneratedDatabaseTypes } from "./type-generator.js";
|
|
6
|
+
import { GENERATED_FILE_HEADER } from "./type-generator.js";
|
|
7
|
+
|
|
8
|
+
const writeTableFiles = async (
|
|
9
|
+
tableDirectory: string,
|
|
10
|
+
tableSources: ReadonlyMap<string, string>
|
|
11
|
+
): Promise<void> => {
|
|
12
|
+
await Promise.all(
|
|
13
|
+
[...tableSources].map(([fileName, source]) =>
|
|
14
|
+
writeFile(path.join(tableDirectory, fileName), source, "utf-8")
|
|
15
|
+
)
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const removeStaleTableFile = async (
|
|
20
|
+
tableDirectory: string,
|
|
21
|
+
expectedFiles: ReadonlySet<string>,
|
|
22
|
+
entry: Dirent
|
|
23
|
+
): Promise<void> => {
|
|
24
|
+
const isStaleTypeFile =
|
|
25
|
+
entry.isFile() &&
|
|
26
|
+
entry.name.endsWith(".ts") &&
|
|
27
|
+
!expectedFiles.has(entry.name);
|
|
28
|
+
if (!isStaleTypeFile) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const filePath = path.join(tableDirectory, entry.name);
|
|
33
|
+
const source = await readFile(filePath, "utf-8");
|
|
34
|
+
if (source.startsWith(GENERATED_FILE_HEADER)) {
|
|
35
|
+
await unlink(filePath);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const removeStaleTableFiles = async (
|
|
40
|
+
tableDirectory: string,
|
|
41
|
+
expectedFiles: ReadonlySet<string>
|
|
42
|
+
): Promise<void> => {
|
|
43
|
+
const entries = await readdir(tableDirectory, { withFileTypes: true });
|
|
44
|
+
await Promise.all(
|
|
45
|
+
entries.map((entry) => {
|
|
46
|
+
return removeStaleTableFile(tableDirectory, expectedFiles, entry);
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Write the database types and remove table files for dropped tables.
|
|
53
|
+
*
|
|
54
|
+
* @param outputDirectory - The directory that contains `db.ts`.
|
|
55
|
+
* @param types - The generated database types.
|
|
56
|
+
*/
|
|
57
|
+
export async function writeGeneratedTypes(
|
|
58
|
+
outputDirectory: string,
|
|
59
|
+
types: GeneratedDatabaseTypes
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const tableDirectory = path.join(outputDirectory, "tables");
|
|
62
|
+
await mkdir(tableDirectory, { recursive: true });
|
|
63
|
+
await writeFile(
|
|
64
|
+
path.join(outputDirectory, "db.ts"),
|
|
65
|
+
types.databaseSource,
|
|
66
|
+
"utf-8"
|
|
67
|
+
);
|
|
68
|
+
await writeTableFiles(tableDirectory, types.tableSources);
|
|
69
|
+
await removeStaleTableFiles(
|
|
70
|
+
tableDirectory,
|
|
71
|
+
new Set(types.tableSources.keys())
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const MIGRATION_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
5
|
+
|
|
6
|
+
const migrationSource = `// oxlint-disable typescript/no-explicit-any
|
|
7
|
+
import type { DSQLKysely } from "../../../vendor/kysely-dsql/dsql/dsql-kysely";
|
|
8
|
+
|
|
9
|
+
export async function up(db: DSQLKysely<any>): Promise<void> {
|
|
10
|
+
// Add the forward schema change.
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function down(db: DSQLKysely<any>): Promise<void> {
|
|
14
|
+
// Add the reverse schema change.
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Supplies the inputs for one migration file.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const options: CreateMigrationFileOptions = {
|
|
24
|
+
* migrationDirectory: "migrations",
|
|
25
|
+
* name: "add-person-status",
|
|
26
|
+
* timestamp: 1_787_403_659_643,
|
|
27
|
+
* };
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export interface CreateMigrationFileOptions {
|
|
31
|
+
/** The directory for the migration file. */
|
|
32
|
+
readonly migrationDirectory: string;
|
|
33
|
+
/** The lowercase hyphen-case migration name. */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/** The optional millisecond timestamp. */
|
|
36
|
+
readonly timestamp?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates one typed migration file without replacement.
|
|
41
|
+
*
|
|
42
|
+
* @param options - The migration file options.
|
|
43
|
+
* @returns The absolute path of the new migration file.
|
|
44
|
+
* @throws {Error} If the name is invalid or the file cannot be created.
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const filePath = await createMigrationFile({
|
|
48
|
+
* migrationDirectory: "migrations",
|
|
49
|
+
* name: "add-person-status",
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export async function createMigrationFile(
|
|
54
|
+
options: CreateMigrationFileOptions,
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
if (!MIGRATION_NAME_PATTERN.test(options.name)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"Migration name must use lowercase letters, digits, and single hyphens.",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const timestamp = options.timestamp ?? Date.now();
|
|
63
|
+
const migrationDirectory = path.resolve(options.migrationDirectory);
|
|
64
|
+
const filePath = path.join(
|
|
65
|
+
migrationDirectory,
|
|
66
|
+
`${timestamp}_${options.name}.ts`,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await mkdir(migrationDirectory, { recursive: true });
|
|
70
|
+
await writeFile(filePath, migrationSource, { encoding: "utf-8", flag: "wx" });
|
|
71
|
+
|
|
72
|
+
return filePath;
|
|
73
|
+
}
|