create-monolayer-app 0.1.1 → 0.2.0

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.
Files changed (37) hide show
  1. package/bin/index.js +1 -1
  2. package/package.json +1 -1
  3. package/template/_gitignore +5 -0
  4. package/template/lib/db/client.ts +30 -0
  5. package/template/lib/db/db.ts +4 -0
  6. package/template/oxlint.config.ts +11 -0
  7. package/template/package.json +23 -3
  8. package/template/tsconfig.json +3 -2
  9. package/template/vendor/kysely-dsql/dsql/adapter.ts +15 -0
  10. package/template/vendor/kysely-dsql/dsql/alter-column-builder.ts +105 -0
  11. package/template/vendor/kysely-dsql/dsql/alter-table-builders.ts +195 -0
  12. package/template/vendor/kysely-dsql/dsql/compatibility-rules.ts +235 -0
  13. package/template/vendor/kysely-dsql/dsql/create-column-builder.ts +187 -0
  14. package/template/vendor/kysely-dsql/dsql/create-table-builder.ts +115 -0
  15. package/template/vendor/kysely-dsql/dsql/dialect.ts +41 -0
  16. package/template/vendor/kysely-dsql/dsql/dsql-kysely.ts +93 -0
  17. package/template/vendor/kysely-dsql/dsql/dsql-token.ts +29 -0
  18. package/template/vendor/kysely-dsql/dsql/index-view-builders.ts +206 -0
  19. package/template/vendor/kysely-dsql/dsql/pool.ts +40 -0
  20. package/template/vendor/kysely-dsql/dsql/query-compiler-error.ts +18 -0
  21. package/template/vendor/kysely-dsql/dsql/query-compiler.test.ts +765 -0
  22. package/template/vendor/kysely-dsql/dsql/query-compiler.ts +259 -0
  23. package/template/vendor/kysely-dsql/dsql/query-validation.test.ts +518 -0
  24. package/template/vendor/kysely-dsql/dsql/query-validation.ts +128 -0
  25. package/template/vendor/kysely-dsql/dsql/raw-statement.test.ts +167 -0
  26. package/template/vendor/kysely-dsql/dsql/raw-statement.ts +51 -0
  27. package/template/vendor/kysely-dsql/dsql/schema-module.ts +167 -0
  28. package/template/vendor/kysely-dsql/dsql/types.ts +4 -0
  29. package/template/vendor/kysely-dsql/generator/create-migration.ts +86 -0
  30. package/template/vendor/kysely-dsql/generator/generate-types-from-pglite.ts +40 -0
  31. package/template/vendor/kysely-dsql/generator/generated-types-writer.ts +73 -0
  32. package/template/vendor/kysely-dsql/generator/migration-file.ts +73 -0
  33. package/template/vendor/kysely-dsql/generator/type-generator.ts +330 -0
  34. package/template/vendor/kysely-dsql/plugins/auto-updated-at.plugin.ts +139 -0
  35. package/template/vendor/kysely-dsql/plugins/index.ts +10 -0
  36. package/template/vendor/kysely-dsql/plugins/soft-delete.plugin.ts +217 -0
  37. 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,4 @@
1
+ import type { Kysely } from "kysely";
2
+
3
+ // oxlint-disable-next-line typescript/no-explicit-any
4
+ export type AnyKysely = Kysely<any>;
@@ -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 "../src/client.js";
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
+ }