auth 1.7.0-beta.0 → 1.7.0-beta.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/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Better Auth CLI
2
2
 
3
3
  Better Auth comes with a built-in CLI to help you manage the database schema
4
- needed for both core functionality and plugins.
4
+ needed for both core functionality and plugins, and to create an initial admin user
5
+ for projects using the Admin plugin.
5
6
 
6
7
  ### **Init**
7
8
 
@@ -34,6 +35,16 @@ tool.
34
35
  npx auth@latest migrate
35
36
  ```
36
37
 
38
+ ### **Create Admin**
39
+
40
+ Create the first admin user for an app using the Admin plugin. The command
41
+ uses your Better Auth config and prompts before creating an admin when users
42
+ already exist. The created admin email is marked as verified by default.
43
+
44
+ ```bash title="terminal"
45
+ npx auth@latest create-admin --email admin@example.com --name "Admin" --role admin
46
+ ```
47
+
37
48
  ### **Secret**
38
49
 
39
50
  The CLI also provides a way to generate a secret key for your Better Auth
package/dist/api.d.mts CHANGED
@@ -26,7 +26,7 @@ declare const generateSchema: (opts: {
26
26
  adapter: DBAdapter$1;
27
27
  file?: string;
28
28
  options: BetterAuthOptions;
29
- }) => Promise<SchemaGeneratorResult> | Promise<{
29
+ }) => Promise<SchemaGeneratorResult | {
30
30
  code: string;
31
31
  fileName: string;
32
32
  overwrite: boolean | undefined;
package/dist/api.mjs CHANGED
@@ -1,16 +1,15 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { capitalizeFirstLetter, toSnakeCase } from "@better-auth/core/utils/string";
2
3
  import { initGetFieldName, initGetModelName } from "better-auth/adapters";
3
4
  import { getAuthTables } from "better-auth/db";
4
5
  import prettier from "prettier";
5
6
  import { getMigrations } from "better-auth/db/migration";
6
7
  import fs from "node:fs/promises";
7
8
  import path from "node:path";
8
- import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
9
9
  import { produceSchema } from "@mrleebo/prisma-ast";
10
10
  //#region src/generators/drizzle.ts
11
11
  function convertToSnakeCase(str, camelCase) {
12
- if (camelCase) return str;
13
- return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase();
12
+ return camelCase ? str : toSnakeCase(str);
14
13
  }
15
14
  const generateDrizzleSchema = async ({ options, file, adapter }) => {
16
15
  const tables = getAuthTables(options);
@@ -27,12 +26,22 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
27
26
  schema: tables,
28
27
  usePlural: adapter.options?.adapterConfig?.usePlural
29
28
  });
29
+ const getSingularModelName = initGetModelName({
30
+ schema: tables,
31
+ usePlural: false
32
+ });
30
33
  const getFieldName = initGetFieldName({
31
34
  schema: tables,
32
35
  usePlural: adapter.options?.adapterConfig?.usePlural
33
36
  });
37
+ const isMigrationDisabled = (model) => Object.entries(tables).some(([tableKey, table]) => {
38
+ if (table.disableMigrations !== true) return false;
39
+ const customModelName = table.modelName || tableKey;
40
+ return model === tableKey || model === customModelName || model === getModelName(tableKey) || model === getModelName(customModelName);
41
+ });
34
42
  for (const tableKey in tables) {
35
43
  const table = tables[tableKey];
44
+ if (isMigrationDisabled(tableKey)) continue;
36
45
  const modelName = getModelName(tableKey);
37
46
  const fields = table.fields;
38
47
  function getType(name, field) {
@@ -52,9 +61,9 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
52
61
  }
53
62
  const type = field.type;
54
63
  if (typeof type !== "string") if (Array.isArray(type) && type.every((x) => typeof x === "string")) return {
55
- sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
64
+ sqlite: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
56
65
  pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
57
- mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])`
66
+ mysql: `mysqlEnum('${name}', [${type.map((x) => `'${x}'`).join(", ")}])`
58
67
  }[databaseType];
59
68
  else throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`);
60
69
  const dbTypeMap = {
@@ -134,11 +143,16 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
134
143
  if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
135
144
  else type += `.defaultNow()`;
136
145
  } else if (typeof attr.defaultValue === "string") type += `.default("${attr.defaultValue}")`;
146
+ else if (Array.isArray(attr.defaultValue)) {
147
+ const elements = attr.defaultValue.map((value) => JSON.stringify(value)).join(", ");
148
+ type += `.default([${elements}])`;
149
+ } else if (typeof attr.defaultValue === "object" && attr.defaultValue !== null) type += `.default(${JSON.stringify(attr.defaultValue)})`;
137
150
  else type += `.default(${attr.defaultValue})`;
138
151
  if (attr.onUpdate && attr.type === "date") {
139
152
  if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`;
140
153
  }
141
- return `${fieldName}: ${type}${attr.required !== false ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({
154
+ const referencesDisabledModel = attr.references && isMigrationDisabled(attr.references.model);
155
+ return `${fieldName}: ${type}${attr.required !== false ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references && !referencesDisabledModel ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({
142
156
  model: attr.references.model,
143
157
  field: attr.references.field
144
158
  })}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
@@ -149,6 +163,7 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
149
163
  let relationsString = "";
150
164
  for (const tableKey in tables) {
151
165
  const table = tables[tableKey];
166
+ if (isMigrationDisabled(tableKey)) continue;
152
167
  const modelName = getModelName(tableKey);
153
168
  const oneRelations = [];
154
169
  const manyRelations = [];
@@ -156,7 +171,8 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
156
171
  const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references);
157
172
  for (const [fieldName, field] of foreignFields) {
158
173
  const referencedModel = field.references.model;
159
- const relationKey = getModelName(referencedModel);
174
+ if (isMigrationDisabled(referencedModel)) continue;
175
+ const relationKey = getSingularModelName(referencedModel);
160
176
  const fieldRef = `${getModelName(tableKey)}.${getFieldName({
161
177
  model: tableKey,
162
178
  field: fieldName
@@ -176,7 +192,7 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
176
192
  }
177
193
  });
178
194
  }
179
- const otherModels = Object.entries(tables).filter(([modelName]) => modelName !== tableKey);
195
+ const otherModels = Object.entries(tables).filter(([modelName, otherTable]) => modelName !== tableKey && !otherTable.disableMigrations);
180
196
  const modelRelationsMap = /* @__PURE__ */ new Map();
181
197
  for (const [modelName, otherTable] of otherModels) {
182
198
  const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey));
@@ -337,6 +353,11 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
337
353
  schema: getAuthTables(options),
338
354
  usePlural: false
339
355
  });
356
+ const isMigrationDisabled = (model) => Object.entries(tables).some(([tableKey, table]) => {
357
+ if (table.disableMigrations !== true) return false;
358
+ const customModelName = table.modelName || tableKey;
359
+ return model === tableKey || model === customModelName || model === getModelName(tableKey) || model === getModelName(customModelName);
360
+ });
340
361
  let schemaPrisma = "";
341
362
  if (schemaPrismaExist) schemaPrisma = await fs.readFile(path.join(process.cwd(), filePath), "utf-8");
342
363
  else schemaPrisma = getNewPrisma(provider, process.cwd());
@@ -355,11 +376,13 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
355
376
  });
356
377
  const manyToManyRelations = /* @__PURE__ */ new Map();
357
378
  for (const table in tables) {
379
+ if (isMigrationDisabled(table)) continue;
358
380
  const fields = tables[table]?.fields;
359
381
  for (const field in fields) {
360
382
  const attr = fields[field];
361
383
  if (attr.references) {
362
384
  const referencedOriginalModel = attr.references.model;
385
+ if (isMigrationDisabled(referencedOriginalModel)) continue;
363
386
  const referencedModelNameCap = capitalizeFirstLetter(getModelName(tables[referencedOriginalModel]?.modelName || referencedOriginalModel));
364
387
  if (!manyToManyRelations.has(referencedModelNameCap)) manyToManyRelations.set(referencedModelNameCap, /* @__PURE__ */ new Set());
365
388
  const currentModelNameCap = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
@@ -382,6 +405,7 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
382
405
  }
383
406
  const schema = produceSchema(schemaPrisma, (builder) => {
384
407
  for (const table in tables) {
408
+ if (isMigrationDisabled(table)) continue;
385
409
  const originalTableName = table;
386
410
  const customModelName = tables[table]?.modelName || table;
387
411
  const modelName = capitalizeFirstLetter(getModelName(customModelName));
@@ -405,6 +429,16 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
405
429
  return "Int[]";
406
430
  }
407
431
  }
432
+ function getFieldTypeParts(type) {
433
+ const isArray = type.endsWith("[]");
434
+ const typeWithoutArray = isArray ? type.slice(0, -2) : type;
435
+ const isOptional = typeWithoutArray.endsWith("?");
436
+ return {
437
+ fieldType: isOptional ? typeWithoutArray.slice(0, -1) : typeWithoutArray,
438
+ isArray,
439
+ isOptional
440
+ };
441
+ }
408
442
  const prismaModel = builder.findByType("model", { name: modelName });
409
443
  if (!prismaModel) if (provider === "mongodb") builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`);
410
444
  else {
@@ -417,15 +451,9 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
417
451
  for (const field in fields) {
418
452
  const attr = fields[field];
419
453
  const fieldName = attr.fieldName || field;
420
- if (prismaModel) {
421
- if (builder.findByType("field", {
422
- name: fieldName,
423
- within: prismaModel.properties
424
- })) continue;
425
- }
426
454
  const useUUIDs = options.advanced?.database?.generateId === "uuid";
427
455
  const useNumberId = options.advanced?.database?.generateId === "serial";
428
- const fieldBuilder = builder.model(modelName).field(fieldName, field === "id" && useNumberId ? getType({
456
+ const fieldType = field === "id" && useNumberId ? getType({
429
457
  isBigint: false,
430
458
  isOptional: false,
431
459
  type: "number"
@@ -433,7 +461,27 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
433
461
  isBigint: attr?.bigint || false,
434
462
  isOptional: attr?.required === false,
435
463
  type: attr.references?.field === "id" ? useNumberId ? "number" : "string" : attr.type
436
- }));
464
+ });
465
+ if (prismaModel) {
466
+ const isAlreadyExist = builder.findByType("field", {
467
+ name: fieldName,
468
+ within: prismaModel.properties
469
+ });
470
+ if (isAlreadyExist) {
471
+ if (fieldType && typeof isAlreadyExist.fieldType === "string") {
472
+ const fieldTypeParts = getFieldTypeParts(fieldType);
473
+ const existingFieldTypeParts = getFieldTypeParts(isAlreadyExist.fieldType);
474
+ if ((existingFieldTypeParts.fieldType === "Int" || existingFieldTypeParts.fieldType === "BigInt") && (fieldTypeParts.fieldType === "Int" || fieldTypeParts.fieldType === "BigInt")) {
475
+ isAlreadyExist.fieldType = fieldTypeParts.fieldType;
476
+ isAlreadyExist.optional = fieldTypeParts.isOptional || void 0;
477
+ isAlreadyExist.array = fieldTypeParts.isArray || void 0;
478
+ }
479
+ }
480
+ continue;
481
+ }
482
+ }
483
+ if (!fieldType) throw new Error(`Unsupported Prisma field type for model "${modelName}", field "${fieldName}"${attr.type ? ` (source type: "${attr.type}")` : ""}.`);
484
+ const fieldBuilder = builder.model(modelName).field(fieldName, fieldType);
437
485
  if (field === "id") {
438
486
  fieldBuilder.attribute("id");
439
487
  if (provider === "mongodb") fieldBuilder.attribute(`map("_id")`);
@@ -478,8 +526,9 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
478
526
  if (field === "updatedAt" && attr.onUpdate) fieldBuilder.attribute("updatedAt");
479
527
  else if (attr.onUpdate) {}
480
528
  if (attr.references) {
481
- if (useUUIDs && provider === "postgresql" && attr.references?.field === "id") builder.model(modelName).field(fieldName).attribute(`db.Uuid`);
482
529
  const referencedOriginalModelName = getModelName(attr.references.model);
530
+ if (isMigrationDisabled(referencedOriginalModelName) || isMigrationDisabled(attr.references.model)) continue;
531
+ if (useUUIDs && provider === "postgresql" && attr.references?.field === "id") builder.model(modelName).field(fieldName).attribute(`db.Uuid`);
483
532
  const referencedCustomModelName = tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName;
484
533
  let action = "Cascade";
485
534
  if (attr.references.onDelete === "no action") action = "NoAction";
@@ -565,16 +614,16 @@ const adapters = {
565
614
  drizzle: generateDrizzleSchema,
566
615
  kysely: generateKyselySchema
567
616
  };
568
- const generateSchema = (opts) => {
617
+ const generateSchema = async (opts) => {
569
618
  const adapter = opts.adapter;
570
- const generator = adapter.id in adapters ? adapters[adapter.id] : null;
571
- if (generator) return generator(opts);
572
619
  if (adapter.createSchema) return adapter.createSchema(opts.options, opts.file).then(({ code, path: fileName, overwrite }) => ({
573
620
  code,
574
621
  fileName,
575
622
  overwrite
576
623
  }));
577
- throw new Error(`${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`);
624
+ const generator = adapters[adapter.id] ?? null;
625
+ if (generator) return await generator(opts);
626
+ throw new Error(`${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement the "createSchema" method on the adapter.`);
578
627
  };
579
628
  //#endregion
580
629
  export { adapters, generateDrizzleSchema, generateKyselySchema, generatePrismaSchema, generateSchema };