@beignet/cli 0.0.9 → 0.0.10

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 (66) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +35 -19
  3. package/dist/choices.d.ts +32 -3
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +54 -3
  6. package/dist/choices.js.map +1 -1
  7. package/dist/create-prompts.d.ts +5 -4
  8. package/dist/create-prompts.d.ts.map +1 -1
  9. package/dist/create-prompts.js +22 -4
  10. package/dist/create-prompts.js.map +1 -1
  11. package/dist/create.d.ts +7 -1
  12. package/dist/create.d.ts.map +1 -1
  13. package/dist/create.js +13 -3
  14. package/dist/create.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +25 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/make.d.ts +21 -1
  19. package/dist/make.d.ts.map +1 -1
  20. package/dist/make.js +210 -37
  21. package/dist/make.js.map +1 -1
  22. package/dist/templates/base.d.ts.map +1 -1
  23. package/dist/templates/base.js +79 -9
  24. package/dist/templates/base.js.map +1 -1
  25. package/dist/templates/db/index.d.ts +21 -0
  26. package/dist/templates/db/index.d.ts.map +1 -0
  27. package/dist/templates/db/index.js +14 -0
  28. package/dist/templates/db/index.js.map +1 -0
  29. package/dist/templates/db/mysql.d.ts +4 -0
  30. package/dist/templates/db/mysql.d.ts.map +1 -0
  31. package/dist/templates/db/mysql.js +972 -0
  32. package/dist/templates/db/mysql.js.map +1 -0
  33. package/dist/templates/db/postgres.d.ts +4 -0
  34. package/dist/templates/db/postgres.d.ts.map +1 -0
  35. package/dist/templates/db/postgres.js +863 -0
  36. package/dist/templates/db/postgres.js.map +1 -0
  37. package/dist/templates/db/sqlite.d.ts +3 -0
  38. package/dist/templates/db/sqlite.d.ts.map +1 -0
  39. package/dist/templates/db/sqlite.js +878 -0
  40. package/dist/templates/db/sqlite.js.map +1 -0
  41. package/dist/templates/index.d.ts +5 -5
  42. package/dist/templates/index.d.ts.map +1 -1
  43. package/dist/templates/index.js +21 -20
  44. package/dist/templates/index.js.map +1 -1
  45. package/dist/templates/server.d.ts +3 -3
  46. package/dist/templates/server.d.ts.map +1 -1
  47. package/dist/templates/server.js +149 -97
  48. package/dist/templates/server.js.map +1 -1
  49. package/dist/templates/shared.d.ts +34 -1
  50. package/dist/templates/shared.d.ts.map +1 -1
  51. package/dist/templates/shared.js +45 -0
  52. package/dist/templates/shared.js.map +1 -1
  53. package/package.json +2 -2
  54. package/src/choices.ts +70 -3
  55. package/src/create-prompts.ts +25 -3
  56. package/src/create.ts +24 -2
  57. package/src/index.ts +29 -5
  58. package/src/make.ts +265 -34
  59. package/src/templates/base.ts +95 -9
  60. package/src/templates/db/index.ts +34 -0
  61. package/src/templates/db/mysql.ts +976 -0
  62. package/src/templates/db/postgres.ts +867 -0
  63. package/src/templates/{db.ts → db/sqlite.ts} +31 -152
  64. package/src/templates/index.ts +24 -19
  65. package/src/templates/server.ts +161 -97
  66. package/src/templates/shared.ts +90 -1
package/src/make.ts CHANGED
@@ -278,6 +278,78 @@ type GeneratedFile = {
278
278
 
279
279
  type ResourcePersistence = "memory" | "drizzle";
280
280
 
281
+ /**
282
+ * SQL dialect of the app database. Kept as a local literal union so the
283
+ * generators do not couple to the create-side option types.
284
+ */
285
+ export type DatabaseName = "sqlite" | "postgres" | "mysql";
286
+
287
+ /**
288
+ * Everything generated Drizzle code needs to target one SQL dialect: the
289
+ * provider subpath and database type, the drizzle-orm column builders, and
290
+ * whether mutations can use `.returning(...)`.
291
+ *
292
+ * Timestamps (`createdAt`/`updatedAt`/`deletedAt`) stay ISO-8601 strings in
293
+ * every dialect — deliberate parity with the starter so cursor pagination
294
+ * compares strings the same way everywhere.
295
+ */
296
+ type DrizzleDialect = {
297
+ /** `@beignet/provider-db-drizzle` subpath the repository imports. */
298
+ subpath: DatabaseName;
299
+ /** Database type exported by the provider subpath. */
300
+ dbTypeName: string;
301
+ /** drizzle-orm column-builder module. */
302
+ columnModule: string;
303
+ /** Table factory exported by the column module. */
304
+ tableFunction: string;
305
+ /** Named imports pulled from the column module, sorted. */
306
+ schemaImports: readonly string[];
307
+ /** Whether insert/update/delete support `.returning(...)`. */
308
+ supportsReturning: boolean;
309
+ /** id and foreign-key column builder. */
310
+ idColumn: (columnName: string) => string;
311
+ /** ISO-8601 timestamp column builder. */
312
+ timestampColumn: (columnName: string) => string;
313
+ /** Integer column builder. */
314
+ integerColumn: (columnName: string) => string;
315
+ };
316
+
317
+ const drizzleDialects: Record<DatabaseName, DrizzleDialect> = {
318
+ sqlite: {
319
+ subpath: "sqlite",
320
+ dbTypeName: "DrizzleSqliteDatabase",
321
+ columnModule: "drizzle-orm/sqlite-core",
322
+ tableFunction: "sqliteTable",
323
+ schemaImports: ["integer", "sqliteTable", "text"],
324
+ supportsReturning: true,
325
+ idColumn: (columnName) => `text("${columnName}")`,
326
+ timestampColumn: (columnName) => `text("${columnName}")`,
327
+ integerColumn: (columnName) => `integer("${columnName}")`,
328
+ },
329
+ postgres: {
330
+ subpath: "postgres",
331
+ dbTypeName: "DrizzlePostgresDatabase",
332
+ columnModule: "drizzle-orm/pg-core",
333
+ tableFunction: "pgTable",
334
+ schemaImports: ["integer", "pgTable", "text"],
335
+ supportsReturning: true,
336
+ idColumn: (columnName) => `text("${columnName}")`,
337
+ timestampColumn: (columnName) => `text("${columnName}")`,
338
+ integerColumn: (columnName) => `integer("${columnName}")`,
339
+ },
340
+ mysql: {
341
+ subpath: "mysql",
342
+ dbTypeName: "DrizzleMysqlDatabase",
343
+ columnModule: "drizzle-orm/mysql-core",
344
+ tableFunction: "mysqlTable",
345
+ schemaImports: ["int", "mysqlTable", "text", "varchar"],
346
+ supportsReturning: false,
347
+ idColumn: (columnName) => `varchar("${columnName}", { length: 36 })`,
348
+ timestampColumn: (columnName) => `varchar("${columnName}", { length: 32 })`,
349
+ integerColumn: (columnName) => `int("${columnName}")`,
350
+ },
351
+ };
352
+
281
353
  type IdentifierNames = {
282
354
  input: string;
283
355
  kebab: string;
@@ -437,6 +509,10 @@ async function makeResourceSlice(
437
509
  }
438
510
 
439
511
  const persistence = await detectResourcePersistence(targetDir, config);
512
+ const database =
513
+ persistence === "drizzle"
514
+ ? await detectResourceDatabase(targetDir, config)
515
+ : "sqlite";
440
516
  const generationOptions = resourceGenerationOptions(options, mode);
441
517
  const generatedFiles = resourceFiles(
442
518
  names,
@@ -444,6 +520,7 @@ async function makeResourceSlice(
444
520
  persistence,
445
521
  mode,
446
522
  generationOptions,
523
+ database,
447
524
  );
448
525
  const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
449
526
  force: Boolean(options.force),
@@ -4071,6 +4148,7 @@ function resourceFiles(
4071
4148
  events: false,
4072
4149
  softDelete: false,
4073
4150
  },
4151
+ database: DatabaseName = "sqlite",
4074
4152
  ): GeneratedFile[] {
4075
4153
  const useCaseDir = resourceUseCaseDir(names, config);
4076
4154
  const infraDir = infrastructureDir(config);
@@ -4155,14 +4233,15 @@ function resourceFiles(
4155
4233
  ];
4156
4234
 
4157
4235
  if (persistence === "drizzle") {
4236
+ const dialect = drizzleDialects[database];
4158
4237
  files.push(
4159
4238
  {
4160
4239
  path: drizzleResourceSchemaFilePath(names, config),
4161
- content: drizzleSchemaFile(names, options),
4240
+ content: drizzleSchemaFile(names, options, dialect),
4162
4241
  },
4163
4242
  {
4164
4243
  path: drizzleResourceRepositoryFilePath(names, config),
4165
- content: drizzleRepositoryFile(names, config, mode, options),
4244
+ content: drizzleRepositoryFile(names, config, mode, options, dialect),
4166
4245
  },
4167
4246
  );
4168
4247
  }
@@ -4472,6 +4551,51 @@ async function detectResourcePersistence(
4472
4551
  : "memory";
4473
4552
  }
4474
4553
 
4554
+ const databaseNames = ["sqlite", "postgres", "mysql"] as const;
4555
+
4556
+ /**
4557
+ * Detect the SQL dialect of a Drizzle app from its generated wiring.
4558
+ *
4559
+ * Precedence:
4560
+ * 1. The provider import in the Drizzle repositories file
4561
+ * (`@beignet/provider-db-drizzle/<dialect>`), resolved through the
4562
+ * configured infrastructure paths so custom layouts keep working.
4563
+ * 2. The provider registration tokens in `server/providers.ts`
4564
+ * (`drizzle<Dialect>Provider` / `createDrizzle<Dialect>Provider`).
4565
+ * 3. `"sqlite"`, the starter default.
4566
+ *
4567
+ * Self-healing by design: there is no config field to drift, the detector
4568
+ * reads the same files the app boots from.
4569
+ */
4570
+ export async function detectResourceDatabase(
4571
+ targetDir: string,
4572
+ config: ResolvedBeignetConfig,
4573
+ ): Promise<DatabaseName> {
4574
+ const repositories = await readOptionalFile(
4575
+ path.join(targetDir, drizzleRepositoriesPath(config)),
4576
+ );
4577
+ const providerImport = repositories?.match(
4578
+ /from\s+["']@beignet\/provider-db-drizzle\/(sqlite|postgres|mysql)["']/,
4579
+ );
4580
+ if (providerImport) return providerImport[1] as DatabaseName;
4581
+
4582
+ const providers = await readOptionalFile(
4583
+ path.join(targetDir, providersFilePath(config)),
4584
+ );
4585
+ if (providers !== undefined) {
4586
+ for (const database of databaseNames) {
4587
+ const dialectPascal =
4588
+ database.charAt(0).toUpperCase() + database.slice(1);
4589
+ const tokens = new RegExp(
4590
+ `\\b(?:drizzle${dialectPascal}Provider|createDrizzle${dialectPascal}Provider)\\b`,
4591
+ );
4592
+ if (tokens.test(providers)) return database;
4593
+ }
4594
+ }
4595
+
4596
+ return "sqlite";
4597
+ }
4598
+
4475
4599
  function resourceFeatureDir(
4476
4600
  names: ResourceNames,
4477
4601
  config: ResolvedBeignetConfig,
@@ -5826,16 +5950,17 @@ ${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter
5826
5950
  function drizzleSchemaFile(
5827
5951
  names: ResourceNames,
5828
5952
  options: ResourceGenerationOptions,
5953
+ dialect: DrizzleDialect,
5829
5954
  ): string {
5830
- return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
5831
-
5832
- export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll("-", "_")}", {
5833
- id: text("id").primaryKey(),
5834
- ${options.tenant ? `\ttenantId: text("tenant_id").notNull(),\n` : ""} name: text("name").notNull(),
5835
- version: integer("version").notNull(),
5836
- createdAt: text("created_at").notNull(),
5837
- updatedAt: text("updated_at").notNull(),
5838
- ${options.softDelete ? `\tdeletedAt: text("deleted_at"),\n` : ""}});
5955
+ return `import { ${dialect.schemaImports.join(", ")} } from "${dialect.columnModule}";
5956
+
5957
+ export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
5958
+ id: ${dialect.idColumn("id")}.primaryKey(),
5959
+ ${options.tenant ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
5960
+ version: ${dialect.integerColumn("version")}.notNull(),
5961
+ createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
5962
+ updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
5963
+ ${options.softDelete ? `\tdeletedAt: ${dialect.timestampColumn("deleted_at")},\n` : ""}});
5839
5964
  `;
5840
5965
  }
5841
5966
 
@@ -5861,6 +5986,7 @@ function drizzleRepositoryFile(
5861
5986
  config: ResolvedBeignetConfig,
5862
5987
  mode: ResourceGenerationMode,
5863
5988
  options: ResourceGenerationOptions,
5989
+ dialect: DrizzleDialect,
5864
5990
  ): string {
5865
5991
  const repositoryPortPath = resourcePortFilePath(names, config);
5866
5992
  const schemaPath = drizzleSchemaIndexPath(config);
@@ -5895,10 +6021,134 @@ function drizzleRepositoryFile(
5895
6021
  "sql",
5896
6022
  "type SQL",
5897
6023
  ].filter((name): name is string => Boolean(name));
6024
+ // After a version-guarded update succeeds, re-select by identity only —
6025
+ // matching what `.returning()` yields on the dialects that support it.
6026
+ const updateReselectWhere = options.tenant
6027
+ ? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, input.tenantId))`
6028
+ : `eq(schema.${names.pluralCamel}.id, input.id)`;
6029
+ // drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
6030
+ // `[ResultSetHeader, FieldPacket[]]` tuple typed as `unknown` through the
6031
+ // generic database seam, so generated MySQL code reads affectedRows
6032
+ // defensively and re-selects rows it needs back.
6033
+ const affectedRowsHelper =
6034
+ mode === "resource" && !dialect.supportsReturning
6035
+ ? `
6036
+ // drizzle-orm/mysql2 mutations resolve to [ResultSetHeader, FieldPacket[]],
6037
+ // typed as unknown through the generic database seam, so read affectedRows
6038
+ // defensively.
6039
+ function affectedRows(result: unknown): number {
6040
+ const head: unknown = Array.isArray(result) ? result[0] : undefined;
6041
+ if (head === null || typeof head !== "object") return 0;
6042
+ return "affectedRows" in head && typeof head.affectedRows === "number"
6043
+ ? head.affectedRows
6044
+ : 0;
6045
+ }
6046
+ `
6047
+ : "";
6048
+ const createMethod = dialect.supportsReturning
6049
+ ? ` async create(input) {
6050
+ const now = new Date().toISOString();
6051
+ const [row] = await db
6052
+ .insert(schema.${names.pluralCamel})
6053
+ .values({
6054
+ id: crypto.randomUUID(),
6055
+ ${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
6056
+ version: 1,
6057
+ createdAt: now,
6058
+ updatedAt: now,
6059
+ })
6060
+ .returning();
6061
+
6062
+ if (!row) {
6063
+ throw new Error("Failed to create ${names.singularKebab}.");
6064
+ }
6065
+
6066
+ return to${names.singularPascal}(row);
6067
+ },
6068
+ `
6069
+ : ` async create(input) {
6070
+ const now = new Date().toISOString();
6071
+ const ${names.singularCamel}: ${names.singularPascal} = {
6072
+ id: crypto.randomUUID(),
6073
+ ${options.tenant ? `\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
6074
+ version: 1,
6075
+ createdAt: now,
6076
+ updatedAt: now,
6077
+ };
6078
+ await db.insert(schema.${names.pluralCamel}).values(${names.singularCamel});
6079
+
6080
+ return ${names.singularCamel};
6081
+ },
6082
+ `;
6083
+ const resourceMethods =
6084
+ mode !== "resource"
6085
+ ? ""
6086
+ : dialect.supportsReturning
6087
+ ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {
6088
+ const [row] = await db
6089
+ .select()
6090
+ .from(schema.${names.pluralCamel})
6091
+ .where(${findWhere})
6092
+ .limit(1);
6093
+
6094
+ return row ? to${names.singularPascal}(row) : null;
6095
+ },
6096
+ async update(input) {
6097
+ const [row] = await db
6098
+ .update(schema.${names.pluralCamel})
6099
+ .set({
6100
+ name: input.name,
6101
+ version: input.version + 1,
6102
+ updatedAt: new Date().toISOString(),
6103
+ })
6104
+ .where(${updateWhere})
6105
+ .returning();
6106
+
6107
+ return row ? to${names.singularPascal}(row) : null;
6108
+ },
6109
+ async delete(id: string${options.tenant ? ", filter" : ""}) {
6110
+ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}
6111
+ return rows.length > 0;
6112
+ },
6113
+ `
6114
+ : ` async findById(id: string${options.tenant ? ", filter" : ""}) {
6115
+ const [row] = await db
6116
+ .select()
6117
+ .from(schema.${names.pluralCamel})
6118
+ .where(${findWhere})
6119
+ .limit(1);
6120
+
6121
+ return row ? to${names.singularPascal}(row) : null;
6122
+ },
6123
+ async update(input) {
6124
+ const result = await db
6125
+ .update(schema.${names.pluralCamel})
6126
+ .set({
6127
+ name: input.name,
6128
+ version: input.version + 1,
6129
+ updatedAt: new Date().toISOString(),
6130
+ })
6131
+ .where(${updateWhere});
6132
+
6133
+ if (affectedRows(result) === 0) return null;
6134
+
6135
+ const [row] = await db
6136
+ .select()
6137
+ .from(schema.${names.pluralCamel})
6138
+ .where(${updateReselectWhere})
6139
+ .limit(1);
6140
+
6141
+ return row ? to${names.singularPascal}(row) : null;
6142
+ },
6143
+ async delete(id: string${options.tenant ? ", filter" : ""}) {
6144
+ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst result = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere});\n` : `\t\t\tconst result = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere});\n`}
6145
+ return affectedRows(result) > 0;
6146
+ },
6147
+ `;
5898
6148
 
5899
6149
  return `import type { beignetServerOnly } from "@beignet/core/server-only";
5900
6150
  import { cursorPageResult } from "@beignet/core/pagination";
5901
- import type { DrizzleSqliteDatabase } from "@beignet/provider-db-drizzle/sqlite";
6151
+ import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
5902
6152
  import { ${drizzleImports.join(", ")} } from "drizzle-orm";
5903
6153
  import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
5904
6154
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
@@ -5919,7 +6169,7 @@ ${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
5919
6169
  updatedAt: row.updatedAt,
5920
6170
  };
5921
6171
  }
5922
-
6172
+ ${affectedRowsHelper}
5923
6173
  type List${names.pluralPascal}Query = Parameters<${names.singularPascal}Repository["list"]>[0];
5924
6174
 
5925
6175
  function ${names.singularCamel}SortColumn(query: List${names.pluralPascal}Query) {
@@ -5978,7 +6228,7 @@ function cursorFor${names.singularPascal}(
5978
6228
  }
5979
6229
 
5980
6230
  export function createDrizzle${names.singularPascal}Repository(
5981
- db: DrizzleSqliteDatabase<typeof schema>,
6231
+ db: ${dialect.dbTypeName}<typeof schema>,
5982
6232
  ): ${names.singularPascal}Repository {
5983
6233
  return {
5984
6234
  async list(query) {
@@ -6001,26 +6251,7 @@ export function createDrizzle${names.singularPascal}Repository(
6001
6251
  nextCursor,
6002
6252
  );
6003
6253
  },
6004
- async create(input) {
6005
- const now = new Date().toISOString();
6006
- const [row] = await db
6007
- .insert(schema.${names.pluralCamel})
6008
- .values({
6009
- id: crypto.randomUUID(),
6010
- ${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
6011
- version: 1,
6012
- createdAt: now,
6013
- updatedAt: now,
6014
- })
6015
- .returning();
6016
-
6017
- if (!row) {
6018
- throw new Error("Failed to create ${names.singularKebab}.");
6019
- }
6020
-
6021
- return to${names.singularPascal}(row);
6022
- },
6023
- ${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {\n const [row] = await db\n .select()\n .from(schema.${names.pluralCamel})\n .where(${findWhere})\n .limit(1);\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async update(input) {\n const [row] = await db\n .update(schema.${names.pluralCamel})\n .set({\n name: input.name,\n version: input.version + 1,\n updatedAt: new Date().toISOString(),\n })\n .where(${updateWhere})\n .returning();\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async delete(id: string${options.tenant ? ", filter" : ""}) {\n${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}\n return rows.length > 0;\n },\n` : ""}
6254
+ ${createMethod}${resourceMethods}
6024
6255
  };
6025
6256
  }
6026
6257
  `;
@@ -1,3 +1,4 @@
1
+ import { databaseLocalUrl, databaseStartCommand } from "../choices.js";
1
2
  import { shadcnDependencies, shadcnDevDependencies } from "./shadcn.js";
2
3
  import {
3
4
  externalVersions,
@@ -17,7 +18,6 @@ export function packageJson(ctx: TemplateContext): string {
17
18
  "@beignet/provider-auth-better-auth": ctx.beignetVersion,
18
19
  "@beignet/provider-db-drizzle": ctx.beignetVersion,
19
20
  "@beignet/provider-logger-pino": ctx.beignetVersion,
20
- "@libsql/client": externalVersions.libsqlClient,
21
21
  "better-auth": externalVersions.betterAuth,
22
22
  "drizzle-orm": externalVersions.drizzleOrm,
23
23
  next: externalVersions.next,
@@ -27,6 +27,19 @@ export function packageJson(ctx: TemplateContext): string {
27
27
  zod: externalVersions.zod,
28
28
  };
29
29
 
30
+ // Ship only the selected database driver.
31
+ switch (ctx.database) {
32
+ case "postgres":
33
+ dependencies.pg = externalVersions.pg;
34
+ break;
35
+ case "mysql":
36
+ dependencies.mysql2 = externalVersions.mysql2;
37
+ break;
38
+ default:
39
+ dependencies["@libsql/client"] = externalVersions.libsqlClient;
40
+ break;
41
+ }
42
+
30
43
  if (!ctx.api) {
31
44
  dependencies["@beignet/react-query"] = ctx.beignetVersion;
32
45
  dependencies["@beignet/react-hook-form"] = ctx.beignetVersion;
@@ -61,6 +74,12 @@ export function packageJson(ctx: TemplateContext): string {
61
74
  typescript: externalVersions.typescript,
62
75
  };
63
76
 
77
+ if (ctx.database === "postgres") {
78
+ // PGlite backs infra/db/test-database.ts with in-process Postgres.
79
+ devDependencies["@electric-sql/pglite"] = externalVersions.pglite;
80
+ devDependencies["@types/pg"] = externalVersions.typesPg;
81
+ }
82
+
64
83
  if (!ctx.api) {
65
84
  for (const [name, version] of Object.entries(shadcnDevDependencies)) {
66
85
  devDependencies[name] = version;
@@ -134,6 +153,30 @@ export function readme(ctx: TemplateContext): string {
134
153
  - \`client/\` owns the typed API client, React Query helpers, and the Better Auth client.
135
154
  `;
136
155
 
156
+ const startDatabase =
157
+ ctx.database === "sqlite"
158
+ ? ""
159
+ : `## Start the database
160
+
161
+ \`\`\`bash
162
+ ${databaseStartCommand(ctx.database, ctx.name)}
163
+ \`\`\`
164
+
165
+ The starter's \`.env.example\` points at this local ${ctx.database === "postgres" ? "Postgres" : "MySQL"} container. Point \`${ctx.database === "postgres" ? "POSTGRES_DB_URL" : "MYSQL_DB_URL"}\` at your own server to skip Docker.
166
+
167
+ `;
168
+
169
+ const testingNotes =
170
+ ctx.database === "postgres"
171
+ ? `
172
+ \`infra/db/test-database.ts\` runs repository tests against in-process Postgres via PGlite, so \`${test}\` needs no database server.
173
+ `
174
+ : ctx.database === "mysql"
175
+ ? `
176
+ The starter's shipped tests use in-memory ports and need no database server. Database-backed tests can use \`createTestDatabase()\` from \`infra/db/test-database.ts\`, which requires \`MYSQL_TEST_URL\` (see the docker one-liner in \`.env.example\`), creates a uniquely named database per call, and drops it on close. Gate such tests with \`test.skipIf(!isMysqlTestDatabaseAvailable())\`.
177
+ `
178
+ : "";
179
+
137
180
  return `# ${ctx.name}
138
181
 
139
182
  Beignet app scaffolded with \`@beignet/cli\`.
@@ -145,7 +188,7 @@ ${install}
145
188
  cp .env.example .env.local
146
189
  \`\`\`
147
190
 
148
- ## Prepare the database
191
+ ${startDatabase}## Prepare the database
149
192
 
150
193
  \`\`\`bash
151
194
  ${cli} db migrate
@@ -173,7 +216,7 @@ ${typecheck}
173
216
  \`\`\`
174
217
 
175
218
  \`routes\` shows the contracts Beignet can inspect. \`lint\` checks dependency direction. \`doctor\` catches route, OpenAPI, and resource drift.
176
-
219
+ ${testingNotes}
177
220
  ## Build for production
178
221
 
179
222
  \`\`\`bash
@@ -207,16 +250,42 @@ ${cli} doctor
207
250
  - \`infra/db/schema/\` contains the Drizzle schema, \`drizzle/\` contains the checked-in migrations, and \`infra/todos/\` contains the durable todo repository adapter.
208
251
  - \`server/routes.ts\` keeps the central route registry and OpenAPI contract list.
209
252
  - \`server/context.ts\` declares the context blueprint shared by the server and route tests.
210
- - \`server/providers.ts\` wires devtools, Better Auth, pino, Drizzle/libSQL, and the starter database provider.
253
+ - \`server/providers.ts\` wires devtools, Better Auth, pino, ${
254
+ ctx.database === "sqlite"
255
+ ? "Drizzle/libSQL"
256
+ : ctx.database === "postgres"
257
+ ? "Drizzle/node-postgres"
258
+ : "Drizzle/mysql2"
259
+ }, and the starter database provider.
211
260
  - \`lib/env.ts\` validates deployment configuration at startup.
212
261
  - \`lib/auth.ts\` exposes \`requireUser(ctx)\` for protected use cases.
213
262
  - \`lib/better-auth.ts\` owns Better Auth setup and keeps provider-specific auth details outside use cases.
214
263
  ${shellMap}
215
264
  ## Before deploying
216
265
 
217
- - Keep \`SQLITE_DB_URL=file:local.db\` for local libSQL development or point it at a hosted libSQL database such as Turso.
266
+ - ${
267
+ ctx.database === "sqlite"
268
+ ? "Keep `SQLITE_DB_URL=file:local.db` for local libSQL development or point it at a hosted libSQL database such as Turso."
269
+ : ctx.database === "postgres"
270
+ ? "Point `POSTGRES_DB_URL` at your production Postgres 14+ server and run `" +
271
+ cli +
272
+ " db migrate` against it before starting the app."
273
+ : "Point `MYSQL_DB_URL` at your production MySQL 8.0+ server and run `" +
274
+ cli +
275
+ " db migrate` against it before starting the app."
276
+ }
218
277
  - Run \`${cli} db generate\` and \`${cli} db migrate\` after changing the Drizzle schema.
219
- - Run \`${cli} db reset\` to rebuild a local SQLite database from the checked-in migrations.
278
+ - Run \`${cli} db reset\` to rebuild a local ${
279
+ ctx.database === "sqlite"
280
+ ? "SQLite"
281
+ : ctx.database === "postgres"
282
+ ? "Postgres"
283
+ : "MySQL"
284
+ } database from the checked-in migrations${
285
+ ctx.database === "sqlite"
286
+ ? ""
287
+ : "; it refuses non-local hosts unless `BEIGNET_ALLOW_DATABASE_RESET=true`"
288
+ }.
220
289
  - Remove \`DEVTOOLS_ENABLED=true\` in production unless you add authentication and stricter redaction.
221
290
  - Set \`APP_URL\`, \`BETTER_AUTH_SECRET\`, \`LOG_LEVEL\`, and service-specific integration variables in your hosting environment.
222
291
  - Set \`BETTER_AUTH_TRUSTED_ORIGINS\` before serving auth across multiple origins.
@@ -276,6 +345,25 @@ export function integrationsDoc(ctx: TemplateContext): string {
276
345
  }
277
346
 
278
347
  export function envExample(ctx: TemplateContext): string {
348
+ const databaseLines =
349
+ ctx.database === "sqlite"
350
+ ? [
351
+ "# Use file:local.db for local libSQL or libsql://... for Turso cloud.",
352
+ "SQLITE_DB_URL=file:local.db",
353
+ "# SQLITE_DB_AUTH_TOKEN=",
354
+ ]
355
+ : ctx.database === "postgres"
356
+ ? [
357
+ "# Local Postgres matching the docker one-liner below; point at your own server in production.",
358
+ `# ${databaseStartCommand("postgres", ctx.name)}`,
359
+ `POSTGRES_DB_URL=${databaseLocalUrl("postgres", ctx.name)}`,
360
+ ]
361
+ : [
362
+ "# Local MySQL matching the docker one-liner below; point at your own server in production.",
363
+ `# ${databaseStartCommand("mysql", ctx.name)}`,
364
+ `MYSQL_DB_URL=${databaseLocalUrl("mysql", ctx.name)}`,
365
+ ];
366
+
279
367
  const lines = [
280
368
  "# Copy to .env.local and fill in values for your app.",
281
369
  "APP_URL=http://localhost:3000",
@@ -287,9 +375,7 @@ export function envExample(ctx: TemplateContext): string {
287
375
  "LOG_FORMAT=json",
288
376
  `LOG_SERVICE=${ctx.name}`,
289
377
  "",
290
- "# Use file:local.db for local libSQL or libsql://... for Turso cloud.",
291
- "SQLITE_DB_URL=file:local.db",
292
- "# SQLITE_DB_AUTH_TOKEN=",
378
+ ...databaseLines,
293
379
  "",
294
380
  "BETTER_AUTH_SECRET=local-dev-better-auth-secret-change-me",
295
381
  "BETTER_AUTH_URL=http://localhost:3000",
@@ -0,0 +1,34 @@
1
+ import type { TemplateContext } from "../shared.js";
2
+ import { mysqlDbFiles } from "./mysql.js";
3
+ import { postgresDbFiles } from "./postgres.js";
4
+ import { sqliteDbFiles } from "./sqlite.js";
5
+
6
+ /**
7
+ * Generated database files shared by every dialect. Each dialect module
8
+ * provides the full set so the scaffold surface stays stable across `--db`
9
+ * choices.
10
+ */
11
+ export type DbTemplateFiles = {
12
+ drizzleConfig: string;
13
+ dbSchema: string;
14
+ drizzleTodoRepository: string;
15
+ dbRepositories: string;
16
+ databaseReady: string;
17
+ dbProvider: string;
18
+ dbReset: string;
19
+ dbTestDatabase: string;
20
+ starterMigrationSql: string;
21
+ starterMigrationJournal: string;
22
+ starterMigrationSnapshot: string;
23
+ };
24
+
25
+ export function dbFiles(ctx: TemplateContext): DbTemplateFiles {
26
+ switch (ctx.database) {
27
+ case "postgres":
28
+ return postgresDbFiles(ctx);
29
+ case "mysql":
30
+ return mysqlDbFiles(ctx);
31
+ default:
32
+ return sqliteDbFiles();
33
+ }
34
+ }