@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.
- package/CHANGELOG.md +7 -0
- package/README.md +35 -19
- package/dist/choices.d.ts +32 -3
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +54 -3
- package/dist/choices.js.map +1 -1
- package/dist/create-prompts.d.ts +5 -4
- package/dist/create-prompts.d.ts.map +1 -1
- package/dist/create-prompts.js +22 -4
- package/dist/create-prompts.js.map +1 -1
- package/dist/create.d.ts +7 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +13 -3
- package/dist/create.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -6
- package/dist/index.js.map +1 -1
- package/dist/make.d.ts +21 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +210 -37
- package/dist/make.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +79 -9
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/index.d.ts +21 -0
- package/dist/templates/db/index.d.ts.map +1 -0
- package/dist/templates/db/index.js +14 -0
- package/dist/templates/db/index.js.map +1 -0
- package/dist/templates/db/mysql.d.ts +4 -0
- package/dist/templates/db/mysql.d.ts.map +1 -0
- package/dist/templates/db/mysql.js +972 -0
- package/dist/templates/db/mysql.js.map +1 -0
- package/dist/templates/db/postgres.d.ts +4 -0
- package/dist/templates/db/postgres.d.ts.map +1 -0
- package/dist/templates/db/postgres.js +863 -0
- package/dist/templates/db/postgres.js.map +1 -0
- package/dist/templates/db/sqlite.d.ts +3 -0
- package/dist/templates/db/sqlite.d.ts.map +1 -0
- package/dist/templates/db/sqlite.js +878 -0
- package/dist/templates/db/sqlite.js.map +1 -0
- package/dist/templates/index.d.ts +5 -5
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +21 -20
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts +3 -3
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +149 -97
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +34 -1
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +45 -0
- package/dist/templates/shared.js.map +1 -1
- package/package.json +2 -2
- package/src/choices.ts +70 -3
- package/src/create-prompts.ts +25 -3
- package/src/create.ts +24 -2
- package/src/index.ts +29 -5
- package/src/make.ts +265 -34
- package/src/templates/base.ts +95 -9
- package/src/templates/db/index.ts +34 -0
- package/src/templates/db/mysql.ts +976 -0
- package/src/templates/db/postgres.ts +867 -0
- package/src/templates/{db.ts → db/sqlite.ts} +31 -152
- package/src/templates/index.ts +24 -19
- package/src/templates/server.ts +161 -97
- 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 {
|
|
5831
|
-
|
|
5832
|
-
export const ${names.pluralCamel} =
|
|
5833
|
-
id:
|
|
5834
|
-
${options.tenant ? `\ttenantId:
|
|
5835
|
-
version:
|
|
5836
|
-
createdAt:
|
|
5837
|
-
updatedAt:
|
|
5838
|
-
${options.softDelete ? `\tdeletedAt:
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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
|
`;
|
package/src/templates/base.ts
CHANGED
|
@@ -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,
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|