auth 1.7.0-beta.3 → 1.7.0-beta.4
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 +12 -1
- package/dist/api.mjs +2 -3
- package/dist/index.mjs +1196 -1084
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -10,22 +10,23 @@ import path, { join } from "node:path";
|
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import prompts from "prompts";
|
|
12
12
|
import yoctoSpinner from "yocto-spinner";
|
|
13
|
+
import { APIError, BetterAuthError } from "@better-auth/core/error";
|
|
14
|
+
import { betterAuth } from "better-auth";
|
|
15
|
+
import * as z from "zod";
|
|
16
|
+
import babelPresetReact from "@babel/preset-react";
|
|
17
|
+
import babelPresetTypeScript from "@babel/preset-typescript";
|
|
18
|
+
import { loadConfig } from "c12";
|
|
19
|
+
import { createPathsMatcher, getTsconfig, parseTsconfig } from "get-tsconfig";
|
|
13
20
|
import fs$1 from "node:fs/promises";
|
|
14
21
|
import { createTelemetry, getTelemetryAuthConfig } from "@better-auth/telemetry";
|
|
15
22
|
import { getAdapter } from "better-auth/db/adapter";
|
|
16
|
-
import
|
|
23
|
+
import { capitalizeFirstLetter, toSnakeCase } from "@better-auth/core/utils/string";
|
|
17
24
|
import { initGetFieldName, initGetModelName } from "better-auth/adapters";
|
|
18
25
|
import { getAuthTables } from "better-auth/db";
|
|
19
26
|
import prettier, { format } from "prettier";
|
|
20
27
|
import { getMigrations } from "better-auth/db/migration";
|
|
21
|
-
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
|
|
22
28
|
import { produceSchema } from "@mrleebo/prisma-ast";
|
|
23
29
|
import Crypto from "node:crypto";
|
|
24
|
-
import babelPresetReact from "@babel/preset-react";
|
|
25
|
-
import babelPresetTypeScript from "@babel/preset-typescript";
|
|
26
|
-
import { BetterAuthError } from "@better-auth/core/error";
|
|
27
|
-
import { loadConfig } from "c12";
|
|
28
|
-
import { createPathsMatcher, getTsconfig, parseTsconfig } from "get-tsconfig";
|
|
29
30
|
import open from "open";
|
|
30
31
|
import { env } from "@better-auth/core/env";
|
|
31
32
|
import { log } from "@clack/prompts";
|
|
@@ -577,1129 +578,1237 @@ function showNextSteps(lines) {
|
|
|
577
578
|
}
|
|
578
579
|
const ai = new Command("ai").description("Interactive setup for Agent Auth — AI agent authentication").action(aiAction);
|
|
579
580
|
//#endregion
|
|
580
|
-
//#region src/
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
581
|
+
//#region src/utils/add-cloudflare-modules.ts
|
|
582
|
+
const createModule = () => {
|
|
583
|
+
return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
|
|
584
|
+
const createStub = (label) => {
|
|
585
|
+
const handler = {
|
|
586
|
+
get(_, prop) {
|
|
587
|
+
if (prop === "toString") return () => label;
|
|
588
|
+
if (prop === "valueOf") return () => label;
|
|
589
|
+
if (prop === Symbol.toPrimitive) return () => label;
|
|
590
|
+
if (prop === Symbol.toStringTag) return "Object";
|
|
591
|
+
if (prop === "then") return undefined;
|
|
592
|
+
return createStub(label + "." + String(prop));
|
|
593
|
+
},
|
|
594
|
+
apply(_, __, args) {
|
|
595
|
+
return createStub(label + "()")
|
|
596
|
+
},
|
|
597
|
+
construct() {
|
|
598
|
+
return createStub(label + "#instance");
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
const fn = () => createStub(label + "()");
|
|
602
|
+
return new Proxy(fn, handler);
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
class WorkerEntrypoint {
|
|
606
|
+
constructor(ctx, env) {
|
|
607
|
+
this.ctx = ctx;
|
|
608
|
+
this.env = env;
|
|
609
|
+
}
|
|
584
610
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
611
|
+
|
|
612
|
+
class DurableObject {
|
|
613
|
+
constructor(state, env) {
|
|
614
|
+
this.state = state;
|
|
615
|
+
this.env = env;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
class RpcTarget {
|
|
620
|
+
constructor(value) {
|
|
621
|
+
this.value = value;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const RpcStub = RpcTarget;
|
|
626
|
+
|
|
627
|
+
const env = createStub("env");
|
|
628
|
+
const caches = createStub("caches");
|
|
629
|
+
const scheduler = createStub("scheduler");
|
|
630
|
+
const executionCtx = createStub("executionCtx");
|
|
631
|
+
|
|
632
|
+
export { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, caches, env, executionCtx, scheduler };
|
|
633
|
+
|
|
634
|
+
const defaultExport = {
|
|
635
|
+
DurableObject,
|
|
636
|
+
RpcStub,
|
|
637
|
+
RpcTarget,
|
|
638
|
+
WorkerEntrypoint,
|
|
639
|
+
caches,
|
|
640
|
+
env,
|
|
641
|
+
executionCtx,
|
|
642
|
+
scheduler,
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
export default defaultExport;
|
|
646
|
+
// jiti dirty hack: .unknown
|
|
647
|
+
`)}`;
|
|
648
|
+
};
|
|
649
|
+
const CLOUDFLARE_STUB_MODULE = createModule();
|
|
650
|
+
function addCloudflareModules(aliases, _cwd) {
|
|
651
|
+
if (!aliases["cloudflare:workers"]) aliases["cloudflare:workers"] = CLOUDFLARE_STUB_MODULE;
|
|
652
|
+
if (!aliases["cloudflare:test"]) aliases["cloudflare:test"] = CLOUDFLARE_STUB_MODULE;
|
|
653
|
+
}
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/utils/add-svelte-kit-env-modules.ts
|
|
656
|
+
/**
|
|
657
|
+
* Adds SvelteKit environment modules and path aliases
|
|
658
|
+
* @param aliases - The aliases object to populate
|
|
659
|
+
* @param cwd - Current working directory (optional, defaults to process.cwd())
|
|
660
|
+
*/
|
|
661
|
+
function addSvelteKitEnvModules(aliases, cwd) {
|
|
662
|
+
const workingDir = cwd || process.cwd();
|
|
663
|
+
aliases["$env/dynamic/private"] = createDataUriModule(createDynamicEnvModule());
|
|
664
|
+
aliases["$env/dynamic/public"] = createDataUriModule(createDynamicEnvModule());
|
|
665
|
+
aliases["$env/static/private"] = createDataUriModule(createStaticEnvModule(filterPrivateEnv("PUBLIC_", "")));
|
|
666
|
+
aliases["$env/static/public"] = createDataUriModule(createStaticEnvModule(filterPublicEnv("PUBLIC_", "")));
|
|
667
|
+
const svelteKitAliases = getSvelteKitPathAliases(workingDir);
|
|
668
|
+
Object.assign(aliases, svelteKitAliases);
|
|
669
|
+
}
|
|
670
|
+
function getSvelteKitPathAliases(cwd) {
|
|
671
|
+
const aliases = {};
|
|
672
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
673
|
+
const svelteConfigPath = path.join(cwd, "svelte.config.js");
|
|
674
|
+
const svelteConfigTsPath = path.join(cwd, "svelte.config.ts");
|
|
675
|
+
let isSvelteKitProject = false;
|
|
676
|
+
if (fs.existsSync(packageJsonPath)) try {
|
|
677
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
678
|
+
isSvelteKitProject = !!{
|
|
679
|
+
...packageJson.dependencies,
|
|
680
|
+
...packageJson.devDependencies
|
|
681
|
+
}["@sveltejs/kit"];
|
|
682
|
+
} catch {}
|
|
683
|
+
if (!isSvelteKitProject) isSvelteKitProject = fs.existsSync(svelteConfigPath) || fs.existsSync(svelteConfigTsPath);
|
|
684
|
+
if (!isSvelteKitProject) return aliases;
|
|
685
|
+
const libPaths = [path.join(cwd, "src", "lib"), path.join(cwd, "lib")];
|
|
686
|
+
for (const libPath of libPaths) if (fs.existsSync(libPath)) {
|
|
687
|
+
aliases["$lib"] = libPath;
|
|
688
|
+
for (const subPath of [
|
|
689
|
+
"server",
|
|
690
|
+
"utils",
|
|
691
|
+
"components",
|
|
692
|
+
"stores"
|
|
693
|
+
]) {
|
|
694
|
+
const subDir = path.join(libPath, subPath);
|
|
695
|
+
if (fs.existsSync(subDir)) aliases[`$lib/${subPath}`] = subDir;
|
|
696
|
+
}
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
aliases["$app/server"] = createDataUriModule(createAppServerModule());
|
|
700
|
+
const customAliases = getSvelteConfigAliases(cwd);
|
|
701
|
+
Object.assign(aliases, customAliases);
|
|
702
|
+
return aliases;
|
|
703
|
+
}
|
|
704
|
+
function getSvelteConfigAliases(cwd) {
|
|
705
|
+
const aliases = {};
|
|
706
|
+
const configPaths = [path.join(cwd, "svelte.config.js"), path.join(cwd, "svelte.config.ts")];
|
|
707
|
+
for (const configPath of configPaths) if (fs.existsSync(configPath)) {
|
|
708
|
+
try {
|
|
709
|
+
const aliasMatch = fs.readFileSync(configPath, "utf-8").match(/alias\s*:\s*\{([^}]+)\}/);
|
|
710
|
+
if (aliasMatch && aliasMatch[1]) {
|
|
711
|
+
const aliasMatches = aliasMatch[1].matchAll(/['"`](\$[^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g);
|
|
712
|
+
for (const match of aliasMatches) {
|
|
713
|
+
const [, alias, target] = match;
|
|
714
|
+
if (alias && target) {
|
|
715
|
+
aliases[alias + "/*"] = path.resolve(cwd, target) + "/*";
|
|
716
|
+
aliases[alias] = path.resolve(cwd, target);
|
|
717
|
+
}
|
|
620
718
|
}
|
|
621
|
-
return `text('${name}')`;
|
|
622
719
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
|
|
626
|
-
pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
|
|
627
|
-
mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])`
|
|
628
|
-
}[databaseType];
|
|
629
|
-
else throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`);
|
|
630
|
-
const dbTypeMap = {
|
|
631
|
-
string: {
|
|
632
|
-
sqlite: `text('${name}')`,
|
|
633
|
-
pg: `text('${name}')`,
|
|
634
|
-
mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : field.sortable ? `varchar('${name}', { length: 255 })` : field.index ? `varchar('${name}', { length: 255 })` : `text('${name}')`
|
|
635
|
-
},
|
|
636
|
-
boolean: {
|
|
637
|
-
sqlite: `integer('${name}', { mode: 'boolean' })`,
|
|
638
|
-
pg: `boolean('${name}')`,
|
|
639
|
-
mysql: `boolean('${name}')`
|
|
640
|
-
},
|
|
641
|
-
number: {
|
|
642
|
-
sqlite: `integer('${name}')`,
|
|
643
|
-
pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`,
|
|
644
|
-
mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')`
|
|
645
|
-
},
|
|
646
|
-
date: {
|
|
647
|
-
sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,
|
|
648
|
-
pg: `timestamp('${name}')`,
|
|
649
|
-
mysql: `timestamp('${name}', { fsp: 3 })`
|
|
650
|
-
},
|
|
651
|
-
"number[]": {
|
|
652
|
-
sqlite: `text('${name}', { mode: "json" })`,
|
|
653
|
-
pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`,
|
|
654
|
-
mysql: `text('${name}', { mode: 'json' })`
|
|
655
|
-
},
|
|
656
|
-
"string[]": {
|
|
657
|
-
sqlite: `text('${name}', { mode: "json" })`,
|
|
658
|
-
pg: `text('${name}').array()`,
|
|
659
|
-
mysql: `text('${name}', { mode: "json" })`
|
|
660
|
-
},
|
|
661
|
-
json: {
|
|
662
|
-
sqlite: `text('${name}', { mode: "json" })`,
|
|
663
|
-
pg: `jsonb('${name}')`,
|
|
664
|
-
mysql: `json('${name}', { mode: "json" })`
|
|
665
|
-
}
|
|
666
|
-
}[type];
|
|
667
|
-
if (!dbTypeMap) throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`);
|
|
668
|
-
return dbTypeMap[databaseType];
|
|
669
|
-
}
|
|
670
|
-
let id = "";
|
|
671
|
-
const useNumberId = options.advanced?.database?.generateId === "serial";
|
|
672
|
-
if (options.advanced?.database?.generateId === "uuid" && databaseType === "pg") id = `uuid("id").default(sql\`pg_catalog.gen_random_uuid()\`).primaryKey()`;
|
|
673
|
-
else if (useNumberId) if (databaseType === "pg") id = `integer("id").generatedByDefaultAsIdentity().primaryKey()`;
|
|
674
|
-
else if (databaseType === "sqlite") id = `integer("id", { mode: "number" }).primaryKey({ autoIncrement: true })`;
|
|
675
|
-
else id = `int("id").autoincrement().primaryKey()`;
|
|
676
|
-
else if (databaseType === "mysql") id = `varchar('id', { length: 36 }).primaryKey()`;
|
|
677
|
-
else if (databaseType === "pg") id = `text('id').primaryKey()`;
|
|
678
|
-
else id = `text('id').primaryKey()`;
|
|
679
|
-
const indexes = [];
|
|
680
|
-
const assignIndexes = (indexes) => {
|
|
681
|
-
if (!indexes.length) return "";
|
|
682
|
-
const code = [`, (table) => [`];
|
|
683
|
-
for (const index of indexes) code.push(` ${index.type}("${index.name}").on(table.${index.on}),`);
|
|
684
|
-
code.push(`]`);
|
|
685
|
-
return code.join("\n");
|
|
686
|
-
};
|
|
687
|
-
const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
|
|
688
|
-
id: ${id},
|
|
689
|
-
${Object.keys(fields).map((field) => {
|
|
690
|
-
const attr = fields[field];
|
|
691
|
-
const fieldName = attr.fieldName || field;
|
|
692
|
-
let type = getType(fieldName, attr);
|
|
693
|
-
if (attr.index && !attr.unique) indexes.push({
|
|
694
|
-
type: "index",
|
|
695
|
-
name: `${modelName}_${fieldName}_idx`,
|
|
696
|
-
on: fieldName
|
|
697
|
-
});
|
|
698
|
-
else if (attr.index && attr.unique) indexes.push({
|
|
699
|
-
type: "uniqueIndex",
|
|
700
|
-
name: `${modelName}_${fieldName}_uidx`,
|
|
701
|
-
on: fieldName
|
|
702
|
-
});
|
|
703
|
-
if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") {
|
|
704
|
-
if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
|
|
705
|
-
else type += `.defaultNow()`;
|
|
706
|
-
} else if (typeof attr.defaultValue === "string") type += `.default("${attr.defaultValue}")`;
|
|
707
|
-
else type += `.default(${attr.defaultValue})`;
|
|
708
|
-
if (attr.onUpdate && attr.type === "date") {
|
|
709
|
-
if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`;
|
|
710
|
-
}
|
|
711
|
-
return `${fieldName}: ${type}${attr.required !== false ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({
|
|
712
|
-
model: attr.references.model,
|
|
713
|
-
field: attr.references.field
|
|
714
|
-
})}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
|
|
715
|
-
}).join(",\n ")}
|
|
716
|
-
}${assignIndexes(indexes)});`;
|
|
717
|
-
code += `\n${schema}\n`;
|
|
718
|
-
}
|
|
719
|
-
let relationsString = "";
|
|
720
|
-
for (const tableKey in tables) {
|
|
721
|
-
const table = tables[tableKey];
|
|
722
|
-
const modelName = getModelName(tableKey);
|
|
723
|
-
const oneRelations = [];
|
|
724
|
-
const manyRelations = [];
|
|
725
|
-
const manyRelationsSet = /* @__PURE__ */ new Set();
|
|
726
|
-
const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references);
|
|
727
|
-
for (const [fieldName, field] of foreignFields) {
|
|
728
|
-
const referencedModel = field.references.model;
|
|
729
|
-
const relationKey = getModelName(referencedModel);
|
|
730
|
-
const fieldRef = `${getModelName(tableKey)}.${getFieldName({
|
|
731
|
-
model: tableKey,
|
|
732
|
-
field: fieldName
|
|
733
|
-
})}`;
|
|
734
|
-
const referenceRef = `${getModelName(referencedModel)}.${getFieldName({
|
|
735
|
-
model: referencedModel,
|
|
736
|
-
field: field.references.field || "id"
|
|
737
|
-
})}`;
|
|
738
|
-
oneRelations.push({
|
|
739
|
-
key: relationKey,
|
|
740
|
-
model: getModelName(referencedModel),
|
|
741
|
-
type: "one",
|
|
742
|
-
reference: {
|
|
743
|
-
field: fieldRef,
|
|
744
|
-
references: referenceRef,
|
|
745
|
-
fieldName
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
const otherModels = Object.entries(tables).filter(([modelName]) => modelName !== tableKey);
|
|
750
|
-
const modelRelationsMap = /* @__PURE__ */ new Map();
|
|
751
|
-
for (const [modelName, otherTable] of otherModels) {
|
|
752
|
-
const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey));
|
|
753
|
-
if (foreignKeysPointingHere.length === 0) continue;
|
|
754
|
-
const hasUnique = foreignKeysPointingHere.some(([_, field]) => !!field.unique);
|
|
755
|
-
const hasMany = foreignKeysPointingHere.some(([_, field]) => !field.unique);
|
|
756
|
-
modelRelationsMap.set(modelName, {
|
|
757
|
-
modelName,
|
|
758
|
-
hasUnique,
|
|
759
|
-
hasMany
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
for (const { modelName, hasMany } of modelRelationsMap.values()) {
|
|
763
|
-
const relationType = hasMany ? "many" : "one";
|
|
764
|
-
let relationKey = getModelName(modelName);
|
|
765
|
-
if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`;
|
|
766
|
-
if (!manyRelationsSet.has(relationKey)) {
|
|
767
|
-
manyRelationsSet.add(relationKey);
|
|
768
|
-
manyRelations.push({
|
|
769
|
-
key: relationKey,
|
|
770
|
-
model: getModelName(modelName),
|
|
771
|
-
type: relationType
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
const relationsByModel = /* @__PURE__ */ new Map();
|
|
776
|
-
for (const relation of oneRelations) if (relation.reference) {
|
|
777
|
-
const modelKey = relation.key;
|
|
778
|
-
if (!relationsByModel.has(modelKey)) relationsByModel.set(modelKey, []);
|
|
779
|
-
relationsByModel.get(modelKey).push(relation);
|
|
780
|
-
}
|
|
781
|
-
const duplicateRelations = [];
|
|
782
|
-
const singleRelations = [];
|
|
783
|
-
for (const [_modelKey, relations] of relationsByModel.entries()) if (relations.length > 1) duplicateRelations.push(...relations);
|
|
784
|
-
else singleRelations.push(relations[0]);
|
|
785
|
-
for (const relation of duplicateRelations) if (relation.reference) {
|
|
786
|
-
const fieldName = relation.reference.fieldName;
|
|
787
|
-
const tableRelation = `export const ${`${modelName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Relations`} = relations(${getModelName(table.modelName)}, ({ one }) => ({
|
|
788
|
-
${relation.key}: one(${relation.model}, {
|
|
789
|
-
fields: [${relation.reference.field}],
|
|
790
|
-
references: [${relation.reference.references}],
|
|
791
|
-
})
|
|
792
|
-
}))`;
|
|
793
|
-
relationsString += `\n${tableRelation}\n`;
|
|
794
|
-
}
|
|
795
|
-
const hasOne = singleRelations.length > 0;
|
|
796
|
-
const hasMany = manyRelations.length > 0;
|
|
797
|
-
if (hasOne && hasMany) {
|
|
798
|
-
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one, many }) => ({
|
|
799
|
-
${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
|
|
800
|
-
fields: [${relation.reference.field}],
|
|
801
|
-
references: [${relation.reference.references}],
|
|
802
|
-
})` : "").filter((x) => x !== "").join(",\n ")}${singleRelations.length > 0 && manyRelations.length > 0 ? "," : ""}
|
|
803
|
-
${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
|
|
804
|
-
}))`;
|
|
805
|
-
relationsString += `\n${tableRelation}\n`;
|
|
806
|
-
} else if (hasOne) {
|
|
807
|
-
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one }) => ({
|
|
808
|
-
${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
|
|
809
|
-
fields: [${relation.reference.field}],
|
|
810
|
-
references: [${relation.reference.references}],
|
|
811
|
-
})` : "").filter((x) => x !== "").join(",\n ")}
|
|
812
|
-
}))`;
|
|
813
|
-
relationsString += `\n${tableRelation}\n`;
|
|
814
|
-
} else if (hasMany) {
|
|
815
|
-
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ many }) => ({
|
|
816
|
-
${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
|
|
817
|
-
}))`;
|
|
818
|
-
relationsString += `\n${tableRelation}\n`;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
code += `\n${relationsString}`;
|
|
822
|
-
return {
|
|
823
|
-
code: await prettier.format(code, { parser: "typescript" }),
|
|
824
|
-
fileName: filePath,
|
|
825
|
-
overwrite: fileExist
|
|
826
|
-
};
|
|
827
|
-
};
|
|
828
|
-
function generateImport({ databaseType, tables, options }) {
|
|
829
|
-
const rootImports = ["relations"];
|
|
830
|
-
const coreImports = [];
|
|
831
|
-
let hasBigint = false;
|
|
832
|
-
let hasJson = false;
|
|
833
|
-
for (const table of Object.values(tables)) {
|
|
834
|
-
for (const field of Object.values(table.fields)) {
|
|
835
|
-
if (field.bigint) hasBigint = true;
|
|
836
|
-
if (field.type === "json") hasJson = true;
|
|
837
|
-
}
|
|
838
|
-
if (hasJson && hasBigint) break;
|
|
839
|
-
}
|
|
840
|
-
const useNumberId = options.advanced?.database?.generateId === "serial";
|
|
841
|
-
const useUUIDs = options.advanced?.database?.generateId === "uuid";
|
|
842
|
-
coreImports.push(`${databaseType}Table`);
|
|
843
|
-
coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text");
|
|
844
|
-
coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
|
|
845
|
-
coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
|
|
846
|
-
if (databaseType === "mysql") {
|
|
847
|
-
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
|
|
848
|
-
if (useNumberId || hasNonBigintNumber) coreImports.push("int");
|
|
849
|
-
if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => typeof field.type !== "string" && Array.isArray(field.type) && field.type.every((x) => typeof x === "string")))) coreImports.push("mysqlEnum");
|
|
850
|
-
} else if (databaseType === "pg") {
|
|
851
|
-
if (useUUIDs) rootImports.push("sql");
|
|
852
|
-
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
|
|
853
|
-
const hasFkToId = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.references?.field === "id"));
|
|
854
|
-
if (hasNonBigintNumber || options.advanced?.database?.generateId === "serial" && hasFkToId) coreImports.push("integer");
|
|
855
|
-
} else coreImports.push("integer");
|
|
856
|
-
if (databaseType === "pg" && useUUIDs) coreImports.push("uuid");
|
|
857
|
-
if (hasJson) {
|
|
858
|
-
if (databaseType === "pg") coreImports.push("jsonb");
|
|
859
|
-
if (databaseType === "mysql") coreImports.push("json");
|
|
860
|
-
}
|
|
861
|
-
if (databaseType === "sqlite" && Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.type === "date" && field.defaultValue && typeof field.defaultValue === "function" && field.defaultValue.toString().includes("new Date()")))) rootImports.push("sql");
|
|
862
|
-
const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique));
|
|
863
|
-
const hasUniqueIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.unique && field.index));
|
|
864
|
-
if (hasIndexes) coreImports.push("index");
|
|
865
|
-
if (hasUniqueIndexes) coreImports.push("uniqueIndex");
|
|
866
|
-
return `${rootImports.length > 0 ? `import { ${rootImports.join(", ")} } from "drizzle-orm";\n` : ""}import { ${coreImports.map((x) => x.trim()).filter((x) => x !== "").join(", ")} } from "drizzle-orm/${databaseType}-core";\n`;
|
|
867
|
-
}
|
|
868
|
-
//#endregion
|
|
869
|
-
//#region src/generators/kysely.ts
|
|
870
|
-
const generateKyselySchema = async ({ options, file }) => {
|
|
871
|
-
const { compileMigrations } = await getMigrations(options);
|
|
872
|
-
const migrations = await compileMigrations();
|
|
873
|
-
return {
|
|
874
|
-
code: migrations.trim() === ";" ? "" : migrations,
|
|
875
|
-
fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
|
|
876
|
-
};
|
|
877
|
-
};
|
|
878
|
-
//#endregion
|
|
879
|
-
//#region src/utils/helper.ts
|
|
880
|
-
async function tryCatch(promise) {
|
|
881
|
-
try {
|
|
882
|
-
return {
|
|
883
|
-
data: await promise,
|
|
884
|
-
error: null
|
|
885
|
-
};
|
|
886
|
-
} catch (error) {
|
|
887
|
-
return {
|
|
888
|
-
data: null,
|
|
889
|
-
error
|
|
890
|
-
};
|
|
720
|
+
} catch {}
|
|
721
|
+
break;
|
|
891
722
|
}
|
|
723
|
+
return aliases;
|
|
892
724
|
}
|
|
893
|
-
|
|
894
|
-
return
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
stdio: "inherit",
|
|
900
|
-
shell: true
|
|
901
|
-
});
|
|
902
|
-
child.on("close", (code, signal) => {
|
|
903
|
-
if (code !== 0 && code !== null) reject(/* @__PURE__ */ new Error(`Exited with code ${code}`));
|
|
904
|
-
else if (signal) reject(/* @__PURE__ */ new Error(`Killed with signal ${signal}`));
|
|
905
|
-
else resolve();
|
|
906
|
-
});
|
|
907
|
-
child.on("error", reject);
|
|
908
|
-
});
|
|
909
|
-
//#endregion
|
|
910
|
-
//#region src/utils/get-package-info.ts
|
|
911
|
-
function getPackageInfo(cwd) {
|
|
912
|
-
const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json");
|
|
913
|
-
return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
725
|
+
function createAppServerModule() {
|
|
726
|
+
return `
|
|
727
|
+
// $app/server stub for CLI compatibility
|
|
728
|
+
export default {};
|
|
729
|
+
// jiti dirty hack: .unknown
|
|
730
|
+
`;
|
|
914
731
|
}
|
|
915
|
-
function
|
|
916
|
-
|
|
917
|
-
const packageInfo = getPackageInfo(cwd);
|
|
918
|
-
const prismaVersion = packageInfo.dependencies?.prisma || packageInfo.devDependencies?.prisma || packageInfo.dependencies?.["@prisma/client"] || packageInfo.devDependencies?.["@prisma/client"];
|
|
919
|
-
if (!prismaVersion) return null;
|
|
920
|
-
const match = prismaVersion.match(/(\d+)/);
|
|
921
|
-
return match ? parseInt(match[1], 10) : null;
|
|
922
|
-
} catch {
|
|
923
|
-
return null;
|
|
924
|
-
}
|
|
732
|
+
function createDataUriModule(module) {
|
|
733
|
+
return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`;
|
|
925
734
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
* @returns true if the package has the dependency
|
|
932
|
-
*/
|
|
933
|
-
function hasDependency(packageJson, dependency) {
|
|
934
|
-
let hasDependency = false;
|
|
935
|
-
if (packageJson.dependencies?.[dependency] || packageJson.devDependencies?.[dependency] || packageJson.peerDependencies?.[dependency] || packageJson.optionalDependencies?.[dependency]) hasDependency = true;
|
|
936
|
-
return hasDependency;
|
|
735
|
+
function createStaticEnvModule(env) {
|
|
736
|
+
return `
|
|
737
|
+
${Object.keys(env).filter((k) => validIdentifier.test(k) && !reserved.has(k)).map((k) => `export const ${k} = ${JSON.stringify(env[k])};`).join("\n")}
|
|
738
|
+
// jiti dirty hack: .unknown
|
|
739
|
+
`;
|
|
937
740
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
741
|
+
function createDynamicEnvModule() {
|
|
742
|
+
return `
|
|
743
|
+
export const env = process.env;
|
|
744
|
+
// jiti dirty hack: .unknown
|
|
745
|
+
`;
|
|
746
|
+
}
|
|
747
|
+
function filterPrivateEnv(publicPrefix, privatePrefix) {
|
|
748
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(privatePrefix) && (publicPrefix === "" || !k.startsWith(publicPrefix))));
|
|
749
|
+
}
|
|
750
|
+
function filterPublicEnv(publicPrefix, privatePrefix) {
|
|
751
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(publicPrefix) && (privatePrefix === "" || !k.startsWith(privatePrefix))));
|
|
752
|
+
}
|
|
753
|
+
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
754
|
+
const reserved = new Set([
|
|
755
|
+
"do",
|
|
756
|
+
"if",
|
|
757
|
+
"in",
|
|
758
|
+
"for",
|
|
759
|
+
"let",
|
|
760
|
+
"new",
|
|
761
|
+
"try",
|
|
762
|
+
"var",
|
|
763
|
+
"case",
|
|
764
|
+
"else",
|
|
765
|
+
"enum",
|
|
766
|
+
"eval",
|
|
767
|
+
"null",
|
|
768
|
+
"this",
|
|
769
|
+
"true",
|
|
770
|
+
"void",
|
|
771
|
+
"with",
|
|
772
|
+
"await",
|
|
773
|
+
"break",
|
|
774
|
+
"catch",
|
|
775
|
+
"class",
|
|
776
|
+
"const",
|
|
777
|
+
"false",
|
|
778
|
+
"super",
|
|
779
|
+
"throw",
|
|
780
|
+
"while",
|
|
781
|
+
"yield",
|
|
782
|
+
"delete",
|
|
783
|
+
"export",
|
|
784
|
+
"import",
|
|
785
|
+
"public",
|
|
786
|
+
"return",
|
|
787
|
+
"static",
|
|
788
|
+
"switch",
|
|
789
|
+
"typeof",
|
|
790
|
+
"default",
|
|
791
|
+
"extends",
|
|
792
|
+
"finally",
|
|
793
|
+
"package",
|
|
794
|
+
"private",
|
|
795
|
+
"continue",
|
|
796
|
+
"debugger",
|
|
797
|
+
"function",
|
|
798
|
+
"arguments",
|
|
799
|
+
"interface",
|
|
800
|
+
"protected",
|
|
801
|
+
"implements",
|
|
802
|
+
"instanceof"
|
|
803
|
+
]);
|
|
804
|
+
//#endregion
|
|
805
|
+
//#region src/utils/get-config.ts
|
|
806
|
+
let possiblePaths$1 = [
|
|
807
|
+
"auth.ts",
|
|
808
|
+
"auth.tsx",
|
|
809
|
+
"auth.js",
|
|
810
|
+
"auth.jsx",
|
|
811
|
+
"auth.server.js",
|
|
812
|
+
"auth.server.ts",
|
|
813
|
+
"auth/index.ts",
|
|
814
|
+
"auth/index.tsx",
|
|
815
|
+
"auth/index.js",
|
|
816
|
+
"auth/index.jsx",
|
|
817
|
+
"auth/index.server.js",
|
|
818
|
+
"auth/index.server.ts"
|
|
819
|
+
];
|
|
820
|
+
possiblePaths$1 = [
|
|
821
|
+
...possiblePaths$1,
|
|
822
|
+
...possiblePaths$1.map((it) => `lib/server/${it}`),
|
|
823
|
+
...possiblePaths$1.map((it) => `server/auth/${it}`),
|
|
824
|
+
...possiblePaths$1.map((it) => `server/${it}`),
|
|
825
|
+
...possiblePaths$1.map((it) => `auth/${it}`),
|
|
826
|
+
...possiblePaths$1.map((it) => `lib/${it}`),
|
|
827
|
+
...possiblePaths$1.map((it) => `utils/${it}`)
|
|
828
|
+
];
|
|
829
|
+
possiblePaths$1 = [
|
|
830
|
+
...possiblePaths$1,
|
|
831
|
+
...possiblePaths$1.map((it) => `src/${it}`),
|
|
832
|
+
...possiblePaths$1.map((it) => `app/${it}`)
|
|
833
|
+
];
|
|
834
|
+
/** Reads `references` from raw tsconfig JSON (stripped out by `parseTsconfig`). */
|
|
835
|
+
function readRawTsconfigReferences(tsconfigPath) {
|
|
836
|
+
try {
|
|
837
|
+
const stripped = fs.readFileSync(tsconfigPath, "utf-8").replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m).replace(/,(?=\s*[}\]])/g, "");
|
|
838
|
+
return JSON.parse(stripped).references;
|
|
839
|
+
} catch {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/** Recursively collects tsconfigs reachable via `references`. */
|
|
844
|
+
function collectReferencedTsconfigs(tsconfigPath, visited = /* @__PURE__ */ new Set()) {
|
|
845
|
+
const result = [];
|
|
846
|
+
const refs = readRawTsconfigReferences(tsconfigPath);
|
|
847
|
+
if (!refs) return result;
|
|
848
|
+
const configDir = path.dirname(tsconfigPath);
|
|
849
|
+
for (const ref of refs) {
|
|
850
|
+
const resolvedRef = path.resolve(configDir, ref.path);
|
|
851
|
+
const refTsconfigPath = resolvedRef.endsWith(".json") ? resolvedRef : path.join(resolvedRef, "tsconfig.json");
|
|
852
|
+
if (visited.has(refTsconfigPath)) continue;
|
|
853
|
+
visited.add(refTsconfigPath);
|
|
854
|
+
try {
|
|
855
|
+
const refConfig = parseTsconfig(refTsconfigPath);
|
|
856
|
+
result.push({
|
|
857
|
+
path: refTsconfigPath,
|
|
858
|
+
config: refConfig
|
|
859
|
+
});
|
|
860
|
+
} catch {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
result.push(...collectReferencedTsconfigs(refTsconfigPath, visited));
|
|
864
|
+
}
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Ordered `paths` matchers from the project tsconfig and any referenced
|
|
869
|
+
* tsconfigs, following TypeScript canonical resolution semantics.
|
|
870
|
+
* @see https://github.com/microsoft/TypeScript/blob/main/src/compiler/moduleNameResolver.ts
|
|
943
871
|
*/
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
if (!
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
if (
|
|
954
|
-
}
|
|
872
|
+
function collectPathsMatchers(cwd) {
|
|
873
|
+
const tsconfig = getTsconfig(cwd, fs.existsSync(path.join(cwd, "tsconfig.json")) ? "tsconfig.json" : "jsconfig.json");
|
|
874
|
+
if (!tsconfig) return [];
|
|
875
|
+
const matchers = [];
|
|
876
|
+
try {
|
|
877
|
+
const mainMatcher = createPathsMatcher(tsconfig);
|
|
878
|
+
if (mainMatcher) matchers.push(mainMatcher);
|
|
879
|
+
for (const refTsconfig of collectReferencedTsconfigs(tsconfig.path)) {
|
|
880
|
+
const refMatcher = createPathsMatcher(refTsconfig);
|
|
881
|
+
if (refMatcher) matchers.push(refMatcher);
|
|
882
|
+
}
|
|
883
|
+
} catch (error) {
|
|
884
|
+
console.error(error);
|
|
885
|
+
throw new BetterAuthError("Error parsing tsconfig.json");
|
|
955
886
|
}
|
|
956
|
-
return
|
|
957
|
-
"lerna.json",
|
|
958
|
-
"turbo.json",
|
|
959
|
-
"nx.json",
|
|
960
|
-
"rush.json"
|
|
961
|
-
].some((indicator) => files.includes(indicator));
|
|
887
|
+
return matchers;
|
|
962
888
|
}
|
|
963
889
|
/**
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
* @param startDir Starting directory
|
|
967
|
-
* @returns Path to monorepo root, or null if not found
|
|
890
|
+
* Source file extensions jiti can load. Shared between the jiti `extensions`
|
|
891
|
+
* option and `resolveCandidateFile` so both stay in sync.
|
|
968
892
|
*/
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
893
|
+
const SOURCE_EXTENSIONS = [
|
|
894
|
+
".ts",
|
|
895
|
+
".tsx",
|
|
896
|
+
".mts",
|
|
897
|
+
".cts",
|
|
898
|
+
".js",
|
|
899
|
+
".jsx",
|
|
900
|
+
".mjs",
|
|
901
|
+
".cjs"
|
|
902
|
+
];
|
|
903
|
+
const SOURCE_EXTENSIONS_SET = new Set(SOURCE_EXTENSIONS);
|
|
904
|
+
/** Probes a candidate as-is, with known extensions, and as a directory index. */
|
|
905
|
+
function resolveCandidateFile(candidate) {
|
|
906
|
+
try {
|
|
907
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
908
|
+
} catch {}
|
|
909
|
+
if (SOURCE_EXTENSIONS_SET.has(path.extname(candidate))) return;
|
|
910
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
911
|
+
const withExt = candidate + ext;
|
|
912
|
+
if (fs.existsSync(withExt)) return withExt;
|
|
913
|
+
}
|
|
914
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
915
|
+
const asIndex = path.join(candidate, `index${ext}`);
|
|
916
|
+
if (fs.existsSync(asIndex)) return asIndex;
|
|
977
917
|
}
|
|
978
|
-
return null;
|
|
979
918
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
const tables = getAuthTables(options);
|
|
985
|
-
const filePath = file || "./prisma/schema.prisma";
|
|
986
|
-
const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
|
|
987
|
-
const getModelName = initGetModelName({
|
|
988
|
-
schema: getAuthTables(options),
|
|
989
|
-
usePlural: adapter.options?.adapterConfig?.usePlural
|
|
990
|
-
});
|
|
991
|
-
const getFieldName = initGetFieldName({
|
|
992
|
-
schema: getAuthTables(options),
|
|
993
|
-
usePlural: false
|
|
994
|
-
});
|
|
995
|
-
let schemaPrisma = "";
|
|
996
|
-
if (schemaPrismaExist) schemaPrisma = await fs$1.readFile(path.join(process.cwd(), filePath), "utf-8");
|
|
997
|
-
else schemaPrisma = getNewPrisma(provider, process.cwd());
|
|
998
|
-
const prismaVersion = getPrismaVersion(process.cwd());
|
|
999
|
-
if (prismaVersion && prismaVersion >= 7 && schemaPrismaExist) schemaPrisma = produceSchema(schemaPrisma, (builder) => {
|
|
1000
|
-
const generator = builder.findByType("generator", { name: "client" });
|
|
1001
|
-
if (generator && generator.properties) {
|
|
1002
|
-
const providerProp = generator.properties.find((prop) => prop.type === "assignment" && prop.key === "provider");
|
|
1003
|
-
if (providerProp && providerProp.value === "\"prisma-client-js\"") providerProp.value = "\"prisma-client\"";
|
|
1004
|
-
}
|
|
1005
|
-
const datasource = builder.findByType("datasource", { name: "db" });
|
|
1006
|
-
if (datasource && datasource.properties) {
|
|
1007
|
-
const urlIndex = datasource.properties.findIndex((prop) => prop.type === "assignment" && prop.key === "url");
|
|
1008
|
-
if (urlIndex !== -1) datasource.properties.splice(urlIndex, 1);
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1011
|
-
const manyToManyRelations = /* @__PURE__ */ new Map();
|
|
1012
|
-
for (const table in tables) {
|
|
1013
|
-
const fields = tables[table]?.fields;
|
|
1014
|
-
for (const field in fields) {
|
|
1015
|
-
const attr = fields[field];
|
|
1016
|
-
if (attr.references) {
|
|
1017
|
-
const referencedOriginalModel = attr.references.model;
|
|
1018
|
-
const referencedModelNameCap = capitalizeFirstLetter(getModelName(tables[referencedOriginalModel]?.modelName || referencedOriginalModel));
|
|
1019
|
-
if (!manyToManyRelations.has(referencedModelNameCap)) manyToManyRelations.set(referencedModelNameCap, /* @__PURE__ */ new Set());
|
|
1020
|
-
const currentModelNameCap = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
|
|
1021
|
-
manyToManyRelations.get(referencedModelNameCap).add(currentModelNameCap);
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
919
|
+
function resolveWithMatchers(specifier, matchers) {
|
|
920
|
+
for (const matcher of matchers) for (const candidate of matcher(specifier)) {
|
|
921
|
+
const resolved = resolveCandidateFile(candidate);
|
|
922
|
+
if (resolved) return resolved;
|
|
1024
923
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Callees whose first string argument is a module specifier. `jitiImport` is
|
|
927
|
+
* a jiti-side preprocessor artifact observed in the AST; revisit on jiti
|
|
928
|
+
* major version bumps (the regression suite catches a rename but not the why).
|
|
929
|
+
*/
|
|
930
|
+
const LOADER_IDENTIFIERS = new Set([
|
|
931
|
+
"require",
|
|
932
|
+
"import",
|
|
933
|
+
"jitiImport"
|
|
934
|
+
]);
|
|
935
|
+
/**
|
|
936
|
+
* Rewrites aliased specifiers at AST level. Required because jiti's `alias`
|
|
937
|
+
* option only supports prefix matching and cannot express mid-path wildcards.
|
|
938
|
+
*
|
|
939
|
+
* Matchers always take precedence over native resolution, mirroring
|
|
940
|
+
* TypeScript's own `paths` → `node_modules` order.
|
|
941
|
+
*/
|
|
942
|
+
function createRewriteImportPathsPlugin(matchers) {
|
|
943
|
+
return ({ types: t }) => {
|
|
944
|
+
const rewrite = (source) => {
|
|
945
|
+
if (!source) return;
|
|
946
|
+
const resolved = resolveWithMatchers(source.value, matchers);
|
|
947
|
+
if (resolved) source.value = resolved;
|
|
948
|
+
};
|
|
949
|
+
return { visitor: {
|
|
950
|
+
ImportDeclaration(p) {
|
|
951
|
+
rewrite(p.node.source);
|
|
952
|
+
},
|
|
953
|
+
ExportNamedDeclaration(p) {
|
|
954
|
+
rewrite(p.node.source);
|
|
955
|
+
},
|
|
956
|
+
ExportAllDeclaration(p) {
|
|
957
|
+
rewrite(p.node.source);
|
|
958
|
+
},
|
|
959
|
+
ImportExpression(p) {
|
|
960
|
+
if (t.isStringLiteral(p.node.source)) rewrite(p.node.source);
|
|
961
|
+
},
|
|
962
|
+
CallExpression(p) {
|
|
963
|
+
const { callee, arguments: args } = p.node;
|
|
964
|
+
const first = args[0];
|
|
965
|
+
if (!t.isStringLiteral(first)) return;
|
|
966
|
+
if (!(t.isIdentifier(callee) && LOADER_IDENTIFIERS.has(callee.name) || t.isImport(callee))) return;
|
|
967
|
+
rewrite(first);
|
|
968
|
+
}
|
|
969
|
+
} };
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
/** Virtual module aliases; real tsconfig paths go through the babel plugin. */
|
|
973
|
+
function getVirtualModuleAliases() {
|
|
974
|
+
const result = {};
|
|
975
|
+
addSvelteKitEnvModules(result);
|
|
976
|
+
addCloudflareModules(result);
|
|
977
|
+
return result;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* .tsx files are not supported by Jiti.
|
|
981
|
+
*/
|
|
982
|
+
const jitiOptions = (cwd) => {
|
|
983
|
+
const matchers = collectPathsMatchers(cwd);
|
|
984
|
+
const plugins = matchers.length > 0 ? [createRewriteImportPathsPlugin(matchers)] : [];
|
|
985
|
+
return {
|
|
986
|
+
transformOptions: { babel: {
|
|
987
|
+
presets: [[babelPresetTypeScript, {
|
|
988
|
+
isTSX: true,
|
|
989
|
+
allExtensions: true
|
|
990
|
+
}], [babelPresetReact, { runtime: "automatic" }]],
|
|
991
|
+
plugins
|
|
992
|
+
} },
|
|
993
|
+
extensions: [...SOURCE_EXTENSIONS],
|
|
994
|
+
alias: getVirtualModuleAliases()
|
|
995
|
+
};
|
|
996
|
+
};
|
|
997
|
+
/**
|
|
998
|
+
* Picks the auth instance from the loaded module, supporting `export const auth`,
|
|
999
|
+
* `export default auth`, and `export default { auth }`. Falls back to the raw
|
|
1000
|
+
* module as a defensive default.
|
|
1001
|
+
*/
|
|
1002
|
+
const resolveAuthModule = (mod) => {
|
|
1003
|
+
const m = mod;
|
|
1004
|
+
return m?.auth ?? m?.default?.auth ?? m?.default ?? mod;
|
|
1005
|
+
};
|
|
1006
|
+
const isServerOnlyError = (e) => typeof e === "object" && e !== null && "message" in e && typeof e.message === "string" && e.message.includes("This module cannot be imported from a Client Component module");
|
|
1007
|
+
const SERVER_ONLY_HINT = `Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`;
|
|
1008
|
+
async function getConfig({ cwd, configPath, shouldThrowOnError = false }) {
|
|
1009
|
+
const fail = (message, error) => {
|
|
1010
|
+
if (shouldThrowOnError) throw error instanceof Error ? error : new Error(message);
|
|
1011
|
+
const log = `[#better-auth]: ${message}`;
|
|
1012
|
+
if (error) console.error(log, error);
|
|
1013
|
+
else console.error(log);
|
|
1014
|
+
process.exit(1);
|
|
1015
|
+
};
|
|
1016
|
+
const load = (configFile) => loadConfig({
|
|
1017
|
+
configFile,
|
|
1018
|
+
dotenv: { fileName: [".env", ".env.local"] },
|
|
1019
|
+
jitiOptions: jitiOptions(cwd),
|
|
1020
|
+
resolveModule: resolveAuthModule,
|
|
1021
|
+
cwd
|
|
1022
|
+
});
|
|
1023
|
+
try {
|
|
1024
|
+
if (configPath) {
|
|
1025
|
+
const resolvedPath = existsSync(configPath) ? configPath : path.join(cwd, configPath);
|
|
1026
|
+
const { config } = await load(resolvedPath);
|
|
1027
|
+
const options = config?.options;
|
|
1028
|
+
if (!options) return fail(`Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`);
|
|
1029
|
+
return options;
|
|
1030
|
+
}
|
|
1031
|
+
for (const possiblePath of possiblePaths$1) {
|
|
1032
|
+
let config;
|
|
1033
|
+
try {
|
|
1034
|
+
({config} = await load(possiblePath));
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
if (isServerOnlyError(e)) return fail(SERVER_ONLY_HINT);
|
|
1037
|
+
return fail("Couldn't read your auth config.", e);
|
|
1035
1038
|
}
|
|
1039
|
+
if (Object.keys(config).length === 0) continue;
|
|
1040
|
+
if (!config.options) return fail("Couldn't read your auth config. Make sure to default export your auth instance or to export as a variable named auth.");
|
|
1041
|
+
return config.options;
|
|
1036
1042
|
}
|
|
1043
|
+
return null;
|
|
1044
|
+
} catch (e) {
|
|
1045
|
+
if (isServerOnlyError(e)) return fail(SERVER_ONLY_HINT);
|
|
1046
|
+
return fail("Couldn't read your auth config.", e);
|
|
1037
1047
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region src/commands/create-admin.ts
|
|
1051
|
+
function exitWithError(message) {
|
|
1052
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
function parseData(data) {
|
|
1056
|
+
if (!data) return void 0;
|
|
1057
|
+
try {
|
|
1058
|
+
const parsed = JSON.parse(data);
|
|
1059
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) exitWithError("--data must be a JSON object.");
|
|
1060
|
+
return parsed;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
if (error instanceof SyntaxError) exitWithError("--data must be valid JSON.");
|
|
1063
|
+
throw error;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async function resolveRequiredInput(options) {
|
|
1067
|
+
let { email, password } = options;
|
|
1068
|
+
if (!email) email = (await prompts({
|
|
1069
|
+
type: "text",
|
|
1070
|
+
name: "email",
|
|
1071
|
+
message: "Admin email"
|
|
1072
|
+
})).email;
|
|
1073
|
+
if (!password) password = (await prompts({
|
|
1074
|
+
type: "password",
|
|
1075
|
+
name: "password",
|
|
1076
|
+
message: "Admin password"
|
|
1077
|
+
})).password;
|
|
1078
|
+
if (!email) exitWithError("Admin email is required.");
|
|
1079
|
+
if (!password) exitWithError("Admin password is required.");
|
|
1080
|
+
return {
|
|
1081
|
+
email,
|
|
1082
|
+
password
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
/** @internal */
|
|
1086
|
+
async function createAdminAction(opts) {
|
|
1087
|
+
const options = z.object({
|
|
1088
|
+
cwd: z.string(),
|
|
1089
|
+
config: z.string().optional(),
|
|
1090
|
+
email: z.string().optional(),
|
|
1091
|
+
password: z.string().optional(),
|
|
1092
|
+
name: z.string().default("Admin"),
|
|
1093
|
+
role: z.string().default("admin"),
|
|
1094
|
+
data: z.string().optional(),
|
|
1095
|
+
emailVerified: z.boolean().default(true),
|
|
1096
|
+
force: z.boolean().optional(),
|
|
1097
|
+
y: z.boolean().optional(),
|
|
1098
|
+
yes: z.boolean().optional()
|
|
1099
|
+
}).parse(opts);
|
|
1100
|
+
const cwd = path.resolve(options.cwd);
|
|
1101
|
+
if (!existsSync(cwd)) exitWithError(`The directory "${cwd}" does not exist.`);
|
|
1102
|
+
if (options.y) {
|
|
1103
|
+
console.warn("WARNING: --y is deprecated. Consider -y or --yes");
|
|
1104
|
+
options.yes = true;
|
|
1105
|
+
}
|
|
1106
|
+
const config = await getConfig({
|
|
1107
|
+
cwd,
|
|
1108
|
+
configPath: options.config
|
|
1109
|
+
});
|
|
1110
|
+
if (!config) exitWithError("No configuration file found. Add an `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.");
|
|
1111
|
+
if (!config.database) exitWithError("No database is configured. Add a persistent database before creating an admin user.");
|
|
1112
|
+
const auth = betterAuth(config);
|
|
1113
|
+
const createUser = auth.api.createUser;
|
|
1114
|
+
if (typeof createUser !== "function") exitWithError("The admin plugin is required. Add `admin()` to your Better Auth plugins before running this command.");
|
|
1115
|
+
const { email, password } = await resolveRequiredInput(options);
|
|
1116
|
+
if (!z.email().safeParse(email).success) exitWithError("Invalid email address.");
|
|
1117
|
+
const data = {
|
|
1118
|
+
...parseData(options.data),
|
|
1119
|
+
emailVerified: options.emailVerified
|
|
1120
|
+
};
|
|
1121
|
+
try {
|
|
1122
|
+
const totalUsers = await (await auth.$context).internalAdapter.countTotalUsers();
|
|
1123
|
+
if (totalUsers > 0 && !options.force && !options.yes) {
|
|
1124
|
+
if (!(await prompts({
|
|
1125
|
+
type: "confirm",
|
|
1126
|
+
name: "confirmed",
|
|
1127
|
+
message: `Found ${totalUsers} existing user${totalUsers === 1 ? "" : "s"}. Create an admin user anyway?`,
|
|
1128
|
+
initial: false
|
|
1129
|
+
})).confirmed) {
|
|
1130
|
+
console.log("Create admin cancelled.");
|
|
1131
|
+
process.exit(0);
|
|
1132
|
+
return;
|
|
1062
1133
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1134
|
+
}
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
exitWithError(`Failed to inspect existing users. Make sure your database is reachable and your Better Auth schema is migrated before running this command.${error instanceof Error ? `\n${error.message}` : ""}`);
|
|
1137
|
+
}
|
|
1138
|
+
try {
|
|
1139
|
+
const result = await createUser({ body: {
|
|
1140
|
+
email,
|
|
1141
|
+
password,
|
|
1142
|
+
name: options.name,
|
|
1143
|
+
role: options.role,
|
|
1144
|
+
data
|
|
1145
|
+
} });
|
|
1146
|
+
console.log(chalk.green("Admin user created successfully."));
|
|
1147
|
+
if (result.user?.id) console.log(`User ID: ${result.user.id}`);
|
|
1148
|
+
console.log(`Email: ${result.user?.email ?? email.toLowerCase()}`);
|
|
1149
|
+
console.log(`Role: ${result.user?.role ?? options.role}`);
|
|
1150
|
+
process.exit(0);
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
if (error instanceof APIError) exitWithError(error.message);
|
|
1153
|
+
if (error instanceof Error) exitWithError(error.message);
|
|
1154
|
+
exitWithError("Failed to create admin user.");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const createAdmin = new Command("create-admin").description("Create an initial admin user").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--config <config>", "the path to the configuration file. defaults to the first configuration file found.").option("--email <email>", "the email address for the admin user").option("--password <password>", "the password for the admin user").option("--name <name>", "the name for the admin user", "Admin").option("--role <role>", "the role to assign to the user", "admin").option("--data <json>", "additional user fields as a JSON object").option("--no-email-verified", "create the admin user with an unverified email").option("--force", "create an admin user even when users already exist", false).option("-y, --yes", "automatically confirm creating an admin when users already exist", false).option("--y", "(deprecated) same as --yes", false).action(createAdminAction);
|
|
1158
|
+
//#endregion
|
|
1159
|
+
//#region src/generators/drizzle.ts
|
|
1160
|
+
function convertToSnakeCase(str, camelCase) {
|
|
1161
|
+
return camelCase ? str : toSnakeCase(str);
|
|
1162
|
+
}
|
|
1163
|
+
const generateDrizzleSchema = async ({ options, file, adapter }) => {
|
|
1164
|
+
const tables = getAuthTables(options);
|
|
1165
|
+
const filePath = file || "./auth-schema.ts";
|
|
1166
|
+
const databaseType = adapter.options?.provider;
|
|
1167
|
+
if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`);
|
|
1168
|
+
const fileExist = existsSync(filePath);
|
|
1169
|
+
let code = generateImport({
|
|
1170
|
+
databaseType,
|
|
1171
|
+
tables,
|
|
1172
|
+
options
|
|
1173
|
+
});
|
|
1174
|
+
const getModelName = initGetModelName({
|
|
1175
|
+
schema: tables,
|
|
1176
|
+
usePlural: adapter.options?.adapterConfig?.usePlural
|
|
1177
|
+
});
|
|
1178
|
+
const getFieldName = initGetFieldName({
|
|
1179
|
+
schema: tables,
|
|
1180
|
+
usePlural: adapter.options?.adapterConfig?.usePlural
|
|
1181
|
+
});
|
|
1182
|
+
for (const tableKey in tables) {
|
|
1183
|
+
const table = tables[tableKey];
|
|
1184
|
+
const modelName = getModelName(tableKey);
|
|
1185
|
+
const fields = table.fields;
|
|
1186
|
+
function getType(name, field) {
|
|
1187
|
+
if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`);
|
|
1188
|
+
name = convertToSnakeCase(name, adapter.options?.camelCase);
|
|
1189
|
+
if (field.references?.field === "id") {
|
|
1066
1190
|
const useNumberId = options.advanced?.database?.generateId === "serial";
|
|
1067
1191
|
const useUUIDs = options.advanced?.database?.generateId === "uuid";
|
|
1068
|
-
if (useNumberId)
|
|
1069
|
-
else if (
|
|
1070
|
-
else
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
const fieldName = attr.fieldName || field;
|
|
1075
|
-
if (prismaModel) {
|
|
1076
|
-
if (builder.findByType("field", {
|
|
1077
|
-
name: fieldName,
|
|
1078
|
-
within: prismaModel.properties
|
|
1079
|
-
})) continue;
|
|
1192
|
+
if (useNumberId) if (databaseType === "pg") return `integer('${name}')`;
|
|
1193
|
+
else if (databaseType === "mysql") return `int('${name}')`;
|
|
1194
|
+
else return `integer('${name}')`;
|
|
1195
|
+
if (useUUIDs && databaseType === "pg") return `uuid('${name}')`;
|
|
1196
|
+
if (field.references.field) {
|
|
1197
|
+
if (databaseType === "mysql") return `varchar('${name}', { length: 36 })`;
|
|
1080
1198
|
}
|
|
1081
|
-
|
|
1082
|
-
const useNumberId = options.advanced?.database?.generateId === "serial";
|
|
1083
|
-
const fieldBuilder = builder.model(modelName).field(fieldName, field === "id" && useNumberId ? getType({
|
|
1084
|
-
isBigint: false,
|
|
1085
|
-
isOptional: false,
|
|
1086
|
-
type: "number"
|
|
1087
|
-
}) : getType({
|
|
1088
|
-
isBigint: attr?.bigint || false,
|
|
1089
|
-
isOptional: attr?.required === false,
|
|
1090
|
-
type: attr.references?.field === "id" ? useNumberId ? "number" : "string" : attr.type
|
|
1091
|
-
}));
|
|
1092
|
-
if (field === "id") {
|
|
1093
|
-
fieldBuilder.attribute("id");
|
|
1094
|
-
if (provider === "mongodb") fieldBuilder.attribute(`map("_id")`);
|
|
1095
|
-
}
|
|
1096
|
-
if (attr.unique) builder.model(modelName).blockAttribute(`unique([${fieldName}])`);
|
|
1097
|
-
if (attr.defaultValue !== void 0) {
|
|
1098
|
-
if (Array.isArray(attr.defaultValue)) {
|
|
1099
|
-
if (attr.type === "json") {
|
|
1100
|
-
if (Object.prototype.toString.call(attr.defaultValue[0]) === "[object Object]") {
|
|
1101
|
-
fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`);
|
|
1102
|
-
continue;
|
|
1103
|
-
}
|
|
1104
|
-
const jsonArray = [];
|
|
1105
|
-
for (const value of attr.defaultValue) jsonArray.push(value);
|
|
1106
|
-
fieldBuilder.attribute(`default("${JSON.stringify(jsonArray).replace(/"/g, "\\\"")}")`);
|
|
1107
|
-
continue;
|
|
1108
|
-
}
|
|
1109
|
-
if (attr.defaultValue.length === 0) {
|
|
1110
|
-
fieldBuilder.attribute(`default([])`);
|
|
1111
|
-
continue;
|
|
1112
|
-
} else if (typeof attr.defaultValue[0] === "string" && attr.type === "string[]") {
|
|
1113
|
-
const valueArray = [];
|
|
1114
|
-
for (const value of attr.defaultValue) valueArray.push(JSON.stringify(value));
|
|
1115
|
-
fieldBuilder.attribute(`default([${valueArray}])`);
|
|
1116
|
-
} else if (typeof attr.defaultValue[0] === "number") {
|
|
1117
|
-
const valueArray = [];
|
|
1118
|
-
for (const value of attr.defaultValue) valueArray.push(`${value}`);
|
|
1119
|
-
fieldBuilder.attribute(`default([${valueArray}])`);
|
|
1120
|
-
}
|
|
1121
|
-
} else if (typeof attr.defaultValue === "object" && !Array.isArray(attr.defaultValue) && attr.defaultValue !== null) {
|
|
1122
|
-
if (Object.entries(attr.defaultValue).length === 0) {
|
|
1123
|
-
fieldBuilder.attribute(`default("{}")`);
|
|
1124
|
-
continue;
|
|
1125
|
-
}
|
|
1126
|
-
fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`);
|
|
1127
|
-
}
|
|
1128
|
-
if (field === "createdAt") fieldBuilder.attribute("default(now())");
|
|
1129
|
-
else if (typeof attr.defaultValue === "string" && provider !== "mysql") fieldBuilder.attribute(`default("${attr.defaultValue}")`);
|
|
1130
|
-
else if (typeof attr.defaultValue === "boolean" || typeof attr.defaultValue === "number") fieldBuilder.attribute(`default(${attr.defaultValue})`);
|
|
1131
|
-
else if (typeof attr.defaultValue === "function") {}
|
|
1132
|
-
}
|
|
1133
|
-
if (field === "updatedAt" && attr.onUpdate) fieldBuilder.attribute("updatedAt");
|
|
1134
|
-
else if (attr.onUpdate) {}
|
|
1135
|
-
if (attr.references) {
|
|
1136
|
-
if (useUUIDs && provider === "postgresql" && attr.references?.field === "id") builder.model(modelName).field(fieldName).attribute(`db.Uuid`);
|
|
1137
|
-
const referencedOriginalModelName = getModelName(attr.references.model);
|
|
1138
|
-
const referencedCustomModelName = tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName;
|
|
1139
|
-
let action = "Cascade";
|
|
1140
|
-
if (attr.references.onDelete === "no action") action = "NoAction";
|
|
1141
|
-
else if (attr.references.onDelete === "set null") action = "SetNull";
|
|
1142
|
-
else if (attr.references.onDelete === "set default") action = "SetDefault";
|
|
1143
|
-
else if (attr.references.onDelete === "restrict") action = "Restrict";
|
|
1144
|
-
const relationField = `relation(fields: [${getFieldName({
|
|
1145
|
-
model: originalTableName,
|
|
1146
|
-
field: fieldName
|
|
1147
|
-
})}], references: [${getFieldName({
|
|
1148
|
-
model: attr.references.model,
|
|
1149
|
-
field: attr.references.field
|
|
1150
|
-
})}], onDelete: ${action})`;
|
|
1151
|
-
builder.model(modelName).field(referencedCustomModelName.toLowerCase(), `${capitalizeFirstLetter(referencedCustomModelName)}${attr.required === false ? "?" : ""}`).attribute(relationField);
|
|
1152
|
-
}
|
|
1153
|
-
if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") builder.model(modelName).field(fieldName).attribute("db.Text");
|
|
1154
|
-
}
|
|
1155
|
-
if (manyToManyRelations.has(modelName)) for (const relatedModel of manyToManyRelations.get(modelName)) {
|
|
1156
|
-
const relatedTableName = Object.keys(tables).find((key) => capitalizeFirstLetter(tables[key]?.modelName || key) === relatedModel);
|
|
1157
|
-
const relatedFields = relatedTableName ? tables[relatedTableName]?.fields : {};
|
|
1158
|
-
const [_fieldKey, fkFieldAttr] = Object.entries(relatedFields || {}).find(([_fieldName, fieldAttr]) => fieldAttr.references && getModelName(fieldAttr.references.model) === getModelName(originalTableName)) || [];
|
|
1159
|
-
const isUnique = fkFieldAttr?.unique === true;
|
|
1160
|
-
const fieldName = isUnique || adapter.options?.usePlural === true ? `${relatedModel.toLowerCase()}` : `${relatedModel.toLowerCase()}s`;
|
|
1161
|
-
if (!builder.findByType("field", {
|
|
1162
|
-
name: fieldName,
|
|
1163
|
-
within: prismaModel?.properties
|
|
1164
|
-
})) builder.model(modelName).field(fieldName, `${relatedModel}${isUnique ? "?" : "[]"}`);
|
|
1199
|
+
return `text('${name}')`;
|
|
1165
1200
|
}
|
|
1166
|
-
const
|
|
1167
|
-
if (
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1201
|
+
const type = field.type;
|
|
1202
|
+
if (typeof type !== "string") if (Array.isArray(type) && type.every((x) => typeof x === "string")) return {
|
|
1203
|
+
sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
|
|
1204
|
+
pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
|
|
1205
|
+
mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])`
|
|
1206
|
+
}[databaseType];
|
|
1207
|
+
else throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`);
|
|
1208
|
+
const dbTypeMap = {
|
|
1209
|
+
string: {
|
|
1210
|
+
sqlite: `text('${name}')`,
|
|
1211
|
+
pg: `text('${name}')`,
|
|
1212
|
+
mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : field.sortable ? `varchar('${name}', { length: 255 })` : field.index ? `varchar('${name}', { length: 255 })` : `text('${name}')`
|
|
1213
|
+
},
|
|
1214
|
+
boolean: {
|
|
1215
|
+
sqlite: `integer('${name}', { mode: 'boolean' })`,
|
|
1216
|
+
pg: `boolean('${name}')`,
|
|
1217
|
+
mysql: `boolean('${name}')`
|
|
1218
|
+
},
|
|
1219
|
+
number: {
|
|
1220
|
+
sqlite: `integer('${name}')`,
|
|
1221
|
+
pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`,
|
|
1222
|
+
mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')`
|
|
1223
|
+
},
|
|
1224
|
+
date: {
|
|
1225
|
+
sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,
|
|
1226
|
+
pg: `timestamp('${name}')`,
|
|
1227
|
+
mysql: `timestamp('${name}', { fsp: 3 })`
|
|
1228
|
+
},
|
|
1229
|
+
"number[]": {
|
|
1230
|
+
sqlite: `text('${name}', { mode: "json" })`,
|
|
1231
|
+
pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`,
|
|
1232
|
+
mysql: `text('${name}', { mode: 'json' })`
|
|
1233
|
+
},
|
|
1234
|
+
"string[]": {
|
|
1235
|
+
sqlite: `text('${name}', { mode: "json" })`,
|
|
1236
|
+
pg: `text('${name}').array()`,
|
|
1237
|
+
mysql: `text('${name}', { mode: "json" })`
|
|
1238
|
+
},
|
|
1239
|
+
json: {
|
|
1240
|
+
sqlite: `text('${name}', { mode: "json" })`,
|
|
1241
|
+
pg: `jsonb('${name}')`,
|
|
1242
|
+
mysql: `json('${name}', { mode: "json" })`
|
|
1178
1243
|
}
|
|
1179
|
-
|
|
1244
|
+
}[type];
|
|
1245
|
+
if (!dbTypeMap) throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`);
|
|
1246
|
+
return dbTypeMap[databaseType];
|
|
1247
|
+
}
|
|
1248
|
+
let id = "";
|
|
1249
|
+
const useNumberId = options.advanced?.database?.generateId === "serial";
|
|
1250
|
+
if (options.advanced?.database?.generateId === "uuid" && databaseType === "pg") id = `uuid("id").default(sql\`pg_catalog.gen_random_uuid()\`).primaryKey()`;
|
|
1251
|
+
else if (useNumberId) if (databaseType === "pg") id = `integer("id").generatedByDefaultAsIdentity().primaryKey()`;
|
|
1252
|
+
else if (databaseType === "sqlite") id = `integer("id", { mode: "number" }).primaryKey({ autoIncrement: true })`;
|
|
1253
|
+
else id = `int("id").autoincrement().primaryKey()`;
|
|
1254
|
+
else if (databaseType === "mysql") id = `varchar('id', { length: 36 }).primaryKey()`;
|
|
1255
|
+
else if (databaseType === "pg") id = `text('id').primaryKey()`;
|
|
1256
|
+
else id = `text('id').primaryKey()`;
|
|
1257
|
+
const indexes = [];
|
|
1258
|
+
const assignIndexes = (indexes) => {
|
|
1259
|
+
if (!indexes.length) return "";
|
|
1260
|
+
const code = [`, (table) => [`];
|
|
1261
|
+
for (const index of indexes) code.push(` ${index.type}("${index.name}").on(table.${index.on}),`);
|
|
1262
|
+
code.push(`]`);
|
|
1263
|
+
return code.join("\n");
|
|
1264
|
+
};
|
|
1265
|
+
const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
|
|
1266
|
+
id: ${id},
|
|
1267
|
+
${Object.keys(fields).map((field) => {
|
|
1268
|
+
const attr = fields[field];
|
|
1269
|
+
const fieldName = attr.fieldName || field;
|
|
1270
|
+
let type = getType(fieldName, attr);
|
|
1271
|
+
if (attr.index && !attr.unique) indexes.push({
|
|
1272
|
+
type: "index",
|
|
1273
|
+
name: `${modelName}_${fieldName}_idx`,
|
|
1274
|
+
on: fieldName
|
|
1275
|
+
});
|
|
1276
|
+
else if (attr.index && attr.unique) indexes.push({
|
|
1277
|
+
type: "uniqueIndex",
|
|
1278
|
+
name: `${modelName}_${fieldName}_uidx`,
|
|
1279
|
+
on: fieldName
|
|
1280
|
+
});
|
|
1281
|
+
if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") {
|
|
1282
|
+
if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
|
|
1283
|
+
else type += `.defaultNow()`;
|
|
1284
|
+
} else if (typeof attr.defaultValue === "string") type += `.default("${attr.defaultValue}")`;
|
|
1285
|
+
else type += `.default(${attr.defaultValue})`;
|
|
1286
|
+
if (attr.onUpdate && attr.type === "date") {
|
|
1287
|
+
if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`;
|
|
1180
1288
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1289
|
+
return `${fieldName}: ${type}${attr.required !== false ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({
|
|
1290
|
+
model: attr.references.model,
|
|
1291
|
+
field: attr.references.field
|
|
1292
|
+
})}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
|
|
1293
|
+
}).join(",\n ")}
|
|
1294
|
+
}${assignIndexes(indexes)});`;
|
|
1295
|
+
code += `\n${schema}\n`;
|
|
1296
|
+
}
|
|
1297
|
+
let relationsString = "";
|
|
1298
|
+
for (const tableKey in tables) {
|
|
1299
|
+
const table = tables[tableKey];
|
|
1300
|
+
const modelName = getModelName(tableKey);
|
|
1301
|
+
const oneRelations = [];
|
|
1302
|
+
const manyRelations = [];
|
|
1303
|
+
const manyRelationsSet = /* @__PURE__ */ new Set();
|
|
1304
|
+
const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references);
|
|
1305
|
+
for (const [fieldName, field] of foreignFields) {
|
|
1306
|
+
const referencedModel = field.references.model;
|
|
1307
|
+
const relationKey = getModelName(referencedModel);
|
|
1308
|
+
const fieldRef = `${getModelName(tableKey)}.${getFieldName({
|
|
1309
|
+
model: tableKey,
|
|
1310
|
+
field: fieldName
|
|
1311
|
+
})}`;
|
|
1312
|
+
const referenceRef = `${getModelName(referencedModel)}.${getFieldName({
|
|
1313
|
+
model: referencedModel,
|
|
1314
|
+
field: field.references.field || "id"
|
|
1315
|
+
})}`;
|
|
1316
|
+
oneRelations.push({
|
|
1317
|
+
key: relationKey,
|
|
1318
|
+
model: getModelName(referencedModel),
|
|
1319
|
+
type: "one",
|
|
1320
|
+
reference: {
|
|
1321
|
+
field: fieldRef,
|
|
1322
|
+
references: referenceRef,
|
|
1323
|
+
fieldName
|
|
1324
|
+
}
|
|
1184
1325
|
});
|
|
1185
|
-
const hasChanged = customModelName !== originalTableName;
|
|
1186
|
-
if (!hasAttribute) builder.model(modelName).blockAttribute("map", `${getModelName(hasChanged ? customModelName : originalTableName)}`);
|
|
1187
1326
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1327
|
+
const otherModels = Object.entries(tables).filter(([modelName]) => modelName !== tableKey);
|
|
1328
|
+
const modelRelationsMap = /* @__PURE__ */ new Map();
|
|
1329
|
+
for (const [modelName, otherTable] of otherModels) {
|
|
1330
|
+
const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey));
|
|
1331
|
+
if (foreignKeysPointingHere.length === 0) continue;
|
|
1332
|
+
const hasUnique = foreignKeysPointingHere.some(([_, field]) => !!field.unique);
|
|
1333
|
+
const hasMany = foreignKeysPointingHere.some(([_, field]) => !field.unique);
|
|
1334
|
+
modelRelationsMap.set(modelName, {
|
|
1335
|
+
modelName,
|
|
1336
|
+
hasUnique,
|
|
1337
|
+
hasMany
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
for (const { modelName, hasMany } of modelRelationsMap.values()) {
|
|
1341
|
+
const relationType = hasMany ? "many" : "one";
|
|
1342
|
+
let relationKey = getModelName(modelName);
|
|
1343
|
+
if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`;
|
|
1344
|
+
if (!manyRelationsSet.has(relationKey)) {
|
|
1345
|
+
manyRelationsSet.add(relationKey);
|
|
1346
|
+
manyRelations.push({
|
|
1347
|
+
key: relationKey,
|
|
1348
|
+
model: getModelName(modelName),
|
|
1349
|
+
type: relationType
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const relationsByModel = /* @__PURE__ */ new Map();
|
|
1354
|
+
for (const relation of oneRelations) if (relation.reference) {
|
|
1355
|
+
const modelKey = relation.key;
|
|
1356
|
+
if (!relationsByModel.has(modelKey)) relationsByModel.set(modelKey, []);
|
|
1357
|
+
relationsByModel.get(modelKey).push(relation);
|
|
1358
|
+
}
|
|
1359
|
+
const duplicateRelations = [];
|
|
1360
|
+
const singleRelations = [];
|
|
1361
|
+
for (const [_modelKey, relations] of relationsByModel.entries()) if (relations.length > 1) duplicateRelations.push(...relations);
|
|
1362
|
+
else singleRelations.push(relations[0]);
|
|
1363
|
+
for (const relation of duplicateRelations) if (relation.reference) {
|
|
1364
|
+
const fieldName = relation.reference.fieldName;
|
|
1365
|
+
const tableRelation = `export const ${`${modelName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Relations`} = relations(${getModelName(table.modelName)}, ({ one }) => ({
|
|
1366
|
+
${relation.key}: one(${relation.model}, {
|
|
1367
|
+
fields: [${relation.reference.field}],
|
|
1368
|
+
references: [${relation.reference.references}],
|
|
1369
|
+
})
|
|
1370
|
+
}))`;
|
|
1371
|
+
relationsString += `\n${tableRelation}\n`;
|
|
1372
|
+
}
|
|
1373
|
+
const hasOne = singleRelations.length > 0;
|
|
1374
|
+
const hasMany = manyRelations.length > 0;
|
|
1375
|
+
if (hasOne && hasMany) {
|
|
1376
|
+
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one, many }) => ({
|
|
1377
|
+
${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
|
|
1378
|
+
fields: [${relation.reference.field}],
|
|
1379
|
+
references: [${relation.reference.references}],
|
|
1380
|
+
})` : "").filter((x) => x !== "").join(",\n ")}${singleRelations.length > 0 && manyRelations.length > 0 ? "," : ""}
|
|
1381
|
+
${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
|
|
1382
|
+
}))`;
|
|
1383
|
+
relationsString += `\n${tableRelation}\n`;
|
|
1384
|
+
} else if (hasOne) {
|
|
1385
|
+
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one }) => ({
|
|
1386
|
+
${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
|
|
1387
|
+
fields: [${relation.reference.field}],
|
|
1388
|
+
references: [${relation.reference.references}],
|
|
1389
|
+
})` : "").filter((x) => x !== "").join(",\n ")}
|
|
1390
|
+
}))`;
|
|
1391
|
+
relationsString += `\n${tableRelation}\n`;
|
|
1392
|
+
} else if (hasMany) {
|
|
1393
|
+
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ many }) => ({
|
|
1394
|
+
${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
|
|
1395
|
+
}))`;
|
|
1396
|
+
relationsString += `\n${tableRelation}\n`;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
code += `\n${relationsString}`;
|
|
1400
|
+
return {
|
|
1401
|
+
code: await prettier.format(code, { parser: "typescript" }),
|
|
1402
|
+
fileName: filePath,
|
|
1403
|
+
overwrite: fileExist
|
|
1404
|
+
};
|
|
1233
1405
|
};
|
|
1406
|
+
function generateImport({ databaseType, tables, options }) {
|
|
1407
|
+
const rootImports = ["relations"];
|
|
1408
|
+
const coreImports = [];
|
|
1409
|
+
let hasBigint = false;
|
|
1410
|
+
let hasJson = false;
|
|
1411
|
+
for (const table of Object.values(tables)) {
|
|
1412
|
+
for (const field of Object.values(table.fields)) {
|
|
1413
|
+
if (field.bigint) hasBigint = true;
|
|
1414
|
+
if (field.type === "json") hasJson = true;
|
|
1415
|
+
}
|
|
1416
|
+
if (hasJson && hasBigint) break;
|
|
1417
|
+
}
|
|
1418
|
+
const useNumberId = options.advanced?.database?.generateId === "serial";
|
|
1419
|
+
const useUUIDs = options.advanced?.database?.generateId === "uuid";
|
|
1420
|
+
coreImports.push(`${databaseType}Table`);
|
|
1421
|
+
coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text");
|
|
1422
|
+
coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
|
|
1423
|
+
coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
|
|
1424
|
+
if (databaseType === "mysql") {
|
|
1425
|
+
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
|
|
1426
|
+
if (useNumberId || hasNonBigintNumber) coreImports.push("int");
|
|
1427
|
+
if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => typeof field.type !== "string" && Array.isArray(field.type) && field.type.every((x) => typeof x === "string")))) coreImports.push("mysqlEnum");
|
|
1428
|
+
} else if (databaseType === "pg") {
|
|
1429
|
+
if (useUUIDs) rootImports.push("sql");
|
|
1430
|
+
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
|
|
1431
|
+
const hasFkToId = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.references?.field === "id"));
|
|
1432
|
+
if (hasNonBigintNumber || options.advanced?.database?.generateId === "serial" && hasFkToId) coreImports.push("integer");
|
|
1433
|
+
} else coreImports.push("integer");
|
|
1434
|
+
if (databaseType === "pg" && useUUIDs) coreImports.push("uuid");
|
|
1435
|
+
if (hasJson) {
|
|
1436
|
+
if (databaseType === "pg") coreImports.push("jsonb");
|
|
1437
|
+
if (databaseType === "mysql") coreImports.push("json");
|
|
1438
|
+
}
|
|
1439
|
+
if (databaseType === "sqlite" && Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.type === "date" && field.defaultValue && typeof field.defaultValue === "function" && field.defaultValue.toString().includes("new Date()")))) rootImports.push("sql");
|
|
1440
|
+
const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique));
|
|
1441
|
+
const hasUniqueIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.unique && field.index));
|
|
1442
|
+
if (hasIndexes) coreImports.push("index");
|
|
1443
|
+
if (hasUniqueIndexes) coreImports.push("uniqueIndex");
|
|
1444
|
+
return `${rootImports.length > 0 ? `import { ${rootImports.join(", ")} } from "drizzle-orm";\n` : ""}import { ${coreImports.map((x) => x.trim()).filter((x) => x !== "").join(", ")} } from "drizzle-orm/${databaseType}-core";\n`;
|
|
1445
|
+
}
|
|
1234
1446
|
//#endregion
|
|
1235
|
-
//#region src/
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
if (prop === Symbol.toPrimitive) return () => label;
|
|
1244
|
-
if (prop === Symbol.toStringTag) return "Object";
|
|
1245
|
-
if (prop === "then") return undefined;
|
|
1246
|
-
return createStub(label + "." + String(prop));
|
|
1247
|
-
},
|
|
1248
|
-
apply(_, __, args) {
|
|
1249
|
-
return createStub(label + "()")
|
|
1250
|
-
},
|
|
1251
|
-
construct() {
|
|
1252
|
-
return createStub(label + "#instance");
|
|
1253
|
-
},
|
|
1254
|
-
};
|
|
1255
|
-
const fn = () => createStub(label + "()");
|
|
1256
|
-
return new Proxy(fn, handler);
|
|
1447
|
+
//#region src/generators/kysely.ts
|
|
1448
|
+
const generateKyselySchema = async ({ options, file }) => {
|
|
1449
|
+
const { compileMigrations } = await getMigrations(options);
|
|
1450
|
+
const migrations = await compileMigrations();
|
|
1451
|
+
return {
|
|
1452
|
+
code: migrations.trim() === ";" ? "" : migrations,
|
|
1453
|
+
fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
|
|
1454
|
+
};
|
|
1257
1455
|
};
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
class RpcTarget {
|
|
1274
|
-
constructor(value) {
|
|
1275
|
-
this.value = value;
|
|
1276
|
-
}
|
|
1456
|
+
//#endregion
|
|
1457
|
+
//#region src/utils/helper.ts
|
|
1458
|
+
async function tryCatch(promise) {
|
|
1459
|
+
try {
|
|
1460
|
+
return {
|
|
1461
|
+
data: await promise,
|
|
1462
|
+
error: null
|
|
1463
|
+
};
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
return {
|
|
1466
|
+
data: null,
|
|
1467
|
+
error
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1277
1470
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const env = createStub("env");
|
|
1282
|
-
const caches = createStub("caches");
|
|
1283
|
-
const scheduler = createStub("scheduler");
|
|
1284
|
-
const executionCtx = createStub("executionCtx");
|
|
1285
|
-
|
|
1286
|
-
export { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, caches, env, executionCtx, scheduler };
|
|
1287
|
-
|
|
1288
|
-
const defaultExport = {
|
|
1289
|
-
DurableObject,
|
|
1290
|
-
RpcStub,
|
|
1291
|
-
RpcTarget,
|
|
1292
|
-
WorkerEntrypoint,
|
|
1293
|
-
caches,
|
|
1294
|
-
env,
|
|
1295
|
-
executionCtx,
|
|
1296
|
-
scheduler,
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
export default defaultExport;
|
|
1300
|
-
// jiti dirty hack: .unknown
|
|
1301
|
-
`)}`;
|
|
1471
|
+
const generateSecretHash = () => {
|
|
1472
|
+
return Crypto.randomBytes(16).toString("hex");
|
|
1302
1473
|
};
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1474
|
+
const spawnCommand = (cmd, cwd = process.cwd()) => new Promise((resolve, reject) => {
|
|
1475
|
+
const child = spawn(cmd, {
|
|
1476
|
+
cwd,
|
|
1477
|
+
stdio: "inherit",
|
|
1478
|
+
shell: true
|
|
1479
|
+
});
|
|
1480
|
+
child.on("close", (code, signal) => {
|
|
1481
|
+
if (code !== 0 && code !== null) reject(/* @__PURE__ */ new Error(`Exited with code ${code}`));
|
|
1482
|
+
else if (signal) reject(/* @__PURE__ */ new Error(`Killed with signal ${signal}`));
|
|
1483
|
+
else resolve();
|
|
1484
|
+
});
|
|
1485
|
+
child.on("error", reject);
|
|
1486
|
+
});
|
|
1308
1487
|
//#endregion
|
|
1309
|
-
//#region src/utils/
|
|
1488
|
+
//#region src/utils/get-package-info.ts
|
|
1489
|
+
function getPackageInfo(cwd) {
|
|
1490
|
+
const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json");
|
|
1491
|
+
return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1492
|
+
}
|
|
1493
|
+
function getPrismaVersion(cwd) {
|
|
1494
|
+
try {
|
|
1495
|
+
const packageInfo = getPackageInfo(cwd);
|
|
1496
|
+
const prismaVersion = packageInfo.dependencies?.prisma || packageInfo.devDependencies?.prisma || packageInfo.dependencies?.["@prisma/client"] || packageInfo.devDependencies?.["@prisma/client"];
|
|
1497
|
+
if (!prismaVersion) return null;
|
|
1498
|
+
const match = prismaVersion.match(/(\d+)/);
|
|
1499
|
+
return match ? parseInt(match[1], 10) : null;
|
|
1500
|
+
} catch {
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1310
1504
|
/**
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
* @param
|
|
1505
|
+
* Checks if a package has a specific dependency.
|
|
1506
|
+
*
|
|
1507
|
+
* @param packageJson The package.json object
|
|
1508
|
+
* @param dependency The dependency to check for
|
|
1509
|
+
* @returns true if the package has the dependency
|
|
1314
1510
|
*/
|
|
1315
|
-
function
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
aliases["$env/static/private"] = createDataUriModule(createStaticEnvModule(filterPrivateEnv("PUBLIC_", "")));
|
|
1320
|
-
aliases["$env/static/public"] = createDataUriModule(createStaticEnvModule(filterPublicEnv("PUBLIC_", "")));
|
|
1321
|
-
const svelteKitAliases = getSvelteKitPathAliases(workingDir);
|
|
1322
|
-
Object.assign(aliases, svelteKitAliases);
|
|
1323
|
-
}
|
|
1324
|
-
function getSvelteKitPathAliases(cwd) {
|
|
1325
|
-
const aliases = {};
|
|
1326
|
-
const packageJsonPath = path.join(cwd, "package.json");
|
|
1327
|
-
const svelteConfigPath = path.join(cwd, "svelte.config.js");
|
|
1328
|
-
const svelteConfigTsPath = path.join(cwd, "svelte.config.ts");
|
|
1329
|
-
let isSvelteKitProject = false;
|
|
1330
|
-
if (fs.existsSync(packageJsonPath)) try {
|
|
1331
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
1332
|
-
isSvelteKitProject = !!{
|
|
1333
|
-
...packageJson.dependencies,
|
|
1334
|
-
...packageJson.devDependencies
|
|
1335
|
-
}["@sveltejs/kit"];
|
|
1336
|
-
} catch {}
|
|
1337
|
-
if (!isSvelteKitProject) isSvelteKitProject = fs.existsSync(svelteConfigPath) || fs.existsSync(svelteConfigTsPath);
|
|
1338
|
-
if (!isSvelteKitProject) return aliases;
|
|
1339
|
-
const libPaths = [path.join(cwd, "src", "lib"), path.join(cwd, "lib")];
|
|
1340
|
-
for (const libPath of libPaths) if (fs.existsSync(libPath)) {
|
|
1341
|
-
aliases["$lib"] = libPath;
|
|
1342
|
-
for (const subPath of [
|
|
1343
|
-
"server",
|
|
1344
|
-
"utils",
|
|
1345
|
-
"components",
|
|
1346
|
-
"stores"
|
|
1347
|
-
]) {
|
|
1348
|
-
const subDir = path.join(libPath, subPath);
|
|
1349
|
-
if (fs.existsSync(subDir)) aliases[`$lib/${subPath}`] = subDir;
|
|
1350
|
-
}
|
|
1351
|
-
break;
|
|
1352
|
-
}
|
|
1353
|
-
aliases["$app/server"] = createDataUriModule(createAppServerModule());
|
|
1354
|
-
const customAliases = getSvelteConfigAliases(cwd);
|
|
1355
|
-
Object.assign(aliases, customAliases);
|
|
1356
|
-
return aliases;
|
|
1511
|
+
function hasDependency(packageJson, dependency) {
|
|
1512
|
+
let hasDependency = false;
|
|
1513
|
+
if (packageJson.dependencies?.[dependency] || packageJson.devDependencies?.[dependency] || packageJson.peerDependencies?.[dependency] || packageJson.optionalDependencies?.[dependency]) hasDependency = true;
|
|
1514
|
+
return hasDependency;
|
|
1357
1515
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1516
|
+
/**
|
|
1517
|
+
* Checks if a directory is a monorepo root by looking for common monorepo indicators.
|
|
1518
|
+
*
|
|
1519
|
+
* @param dir Directory to check
|
|
1520
|
+
* @returns true if the directory appears to be a monorepo root
|
|
1521
|
+
*/
|
|
1522
|
+
async function isMonorepoRoot(dir) {
|
|
1523
|
+
const { data: files } = await tryCatch(fs$1.readdir(dir, "utf-8"));
|
|
1524
|
+
if (!files) return false;
|
|
1525
|
+
if (files.includes("pnpm-workspace.yaml")) return true;
|
|
1526
|
+
if (files.includes("package.json")) {
|
|
1527
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
1528
|
+
const { data } = await tryCatch(fs$1.readFile(packageJsonPath, "utf-8"));
|
|
1529
|
+
if (data) try {
|
|
1530
|
+
const packageJson = JSON.parse(data);
|
|
1531
|
+
if (packageJson.workspaces && (Array.isArray(packageJson.workspaces) || typeof packageJson.workspaces === "object")) return true;
|
|
1374
1532
|
} catch {}
|
|
1375
|
-
break;
|
|
1376
1533
|
}
|
|
1377
|
-
return
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
// jiti dirty hack: .unknown
|
|
1384
|
-
`;
|
|
1385
|
-
}
|
|
1386
|
-
function createDataUriModule(module) {
|
|
1387
|
-
return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`;
|
|
1388
|
-
}
|
|
1389
|
-
function createStaticEnvModule(env) {
|
|
1390
|
-
return `
|
|
1391
|
-
${Object.keys(env).filter((k) => validIdentifier.test(k) && !reserved.has(k)).map((k) => `export const ${k} = ${JSON.stringify(env[k])};`).join("\n")}
|
|
1392
|
-
// jiti dirty hack: .unknown
|
|
1393
|
-
`;
|
|
1394
|
-
}
|
|
1395
|
-
function createDynamicEnvModule() {
|
|
1396
|
-
return `
|
|
1397
|
-
export const env = process.env;
|
|
1398
|
-
// jiti dirty hack: .unknown
|
|
1399
|
-
`;
|
|
1400
|
-
}
|
|
1401
|
-
function filterPrivateEnv(publicPrefix, privatePrefix) {
|
|
1402
|
-
return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(privatePrefix) && (publicPrefix === "" || !k.startsWith(publicPrefix))));
|
|
1534
|
+
return [
|
|
1535
|
+
"lerna.json",
|
|
1536
|
+
"turbo.json",
|
|
1537
|
+
"nx.json",
|
|
1538
|
+
"rush.json"
|
|
1539
|
+
].some((indicator) => files.includes(indicator));
|
|
1403
1540
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1541
|
+
/**
|
|
1542
|
+
* Finds the monorepo root by walking up the directory tree.
|
|
1543
|
+
*
|
|
1544
|
+
* @param startDir Starting directory
|
|
1545
|
+
* @returns Path to monorepo root, or null if not found
|
|
1546
|
+
*/
|
|
1547
|
+
async function findMonorepoRoot(startDir) {
|
|
1548
|
+
let currentDir = path.resolve(startDir);
|
|
1549
|
+
const root = path.parse(currentDir).root;
|
|
1550
|
+
while (currentDir !== root) {
|
|
1551
|
+
if (await isMonorepoRoot(currentDir)) return currentDir;
|
|
1552
|
+
const parentDir = path.dirname(currentDir);
|
|
1553
|
+
if (parentDir === currentDir) break;
|
|
1554
|
+
currentDir = parentDir;
|
|
1555
|
+
}
|
|
1556
|
+
return null;
|
|
1406
1557
|
}
|
|
1407
|
-
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
1408
|
-
const reserved = new Set([
|
|
1409
|
-
"do",
|
|
1410
|
-
"if",
|
|
1411
|
-
"in",
|
|
1412
|
-
"for",
|
|
1413
|
-
"let",
|
|
1414
|
-
"new",
|
|
1415
|
-
"try",
|
|
1416
|
-
"var",
|
|
1417
|
-
"case",
|
|
1418
|
-
"else",
|
|
1419
|
-
"enum",
|
|
1420
|
-
"eval",
|
|
1421
|
-
"null",
|
|
1422
|
-
"this",
|
|
1423
|
-
"true",
|
|
1424
|
-
"void",
|
|
1425
|
-
"with",
|
|
1426
|
-
"await",
|
|
1427
|
-
"break",
|
|
1428
|
-
"catch",
|
|
1429
|
-
"class",
|
|
1430
|
-
"const",
|
|
1431
|
-
"false",
|
|
1432
|
-
"super",
|
|
1433
|
-
"throw",
|
|
1434
|
-
"while",
|
|
1435
|
-
"yield",
|
|
1436
|
-
"delete",
|
|
1437
|
-
"export",
|
|
1438
|
-
"import",
|
|
1439
|
-
"public",
|
|
1440
|
-
"return",
|
|
1441
|
-
"static",
|
|
1442
|
-
"switch",
|
|
1443
|
-
"typeof",
|
|
1444
|
-
"default",
|
|
1445
|
-
"extends",
|
|
1446
|
-
"finally",
|
|
1447
|
-
"package",
|
|
1448
|
-
"private",
|
|
1449
|
-
"continue",
|
|
1450
|
-
"debugger",
|
|
1451
|
-
"function",
|
|
1452
|
-
"arguments",
|
|
1453
|
-
"interface",
|
|
1454
|
-
"protected",
|
|
1455
|
-
"implements",
|
|
1456
|
-
"instanceof"
|
|
1457
|
-
]);
|
|
1458
1558
|
//#endregion
|
|
1459
|
-
//#region src/
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
"
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1559
|
+
//#region src/generators/prisma.ts
|
|
1560
|
+
const generatePrismaSchema = async ({ adapter, options, file }) => {
|
|
1561
|
+
const provider = adapter.options?.provider || "postgresql";
|
|
1562
|
+
const tables = getAuthTables(options);
|
|
1563
|
+
const filePath = file || "./prisma/schema.prisma";
|
|
1564
|
+
const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
|
|
1565
|
+
const getModelName = initGetModelName({
|
|
1566
|
+
schema: getAuthTables(options),
|
|
1567
|
+
usePlural: adapter.options?.adapterConfig?.usePlural
|
|
1568
|
+
});
|
|
1569
|
+
const getFieldName = initGetFieldName({
|
|
1570
|
+
schema: getAuthTables(options),
|
|
1571
|
+
usePlural: false
|
|
1572
|
+
});
|
|
1573
|
+
let schemaPrisma = "";
|
|
1574
|
+
if (schemaPrismaExist) schemaPrisma = await fs$1.readFile(path.join(process.cwd(), filePath), "utf-8");
|
|
1575
|
+
else schemaPrisma = getNewPrisma(provider, process.cwd());
|
|
1576
|
+
const prismaVersion = getPrismaVersion(process.cwd());
|
|
1577
|
+
if (prismaVersion && prismaVersion >= 7 && schemaPrismaExist) schemaPrisma = produceSchema(schemaPrisma, (builder) => {
|
|
1578
|
+
const generator = builder.findByType("generator", { name: "client" });
|
|
1579
|
+
if (generator && generator.properties) {
|
|
1580
|
+
const providerProp = generator.properties.find((prop) => prop.type === "assignment" && prop.key === "provider");
|
|
1581
|
+
if (providerProp && providerProp.value === "\"prisma-client-js\"") providerProp.value = "\"prisma-client\"";
|
|
1582
|
+
}
|
|
1583
|
+
const datasource = builder.findByType("datasource", { name: "db" });
|
|
1584
|
+
if (datasource && datasource.properties) {
|
|
1585
|
+
const urlIndex = datasource.properties.findIndex((prop) => prop.type === "assignment" && prop.key === "url");
|
|
1586
|
+
if (urlIndex !== -1) datasource.properties.splice(urlIndex, 1);
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
const manyToManyRelations = /* @__PURE__ */ new Map();
|
|
1590
|
+
for (const table in tables) {
|
|
1591
|
+
const fields = tables[table]?.fields;
|
|
1592
|
+
for (const field in fields) {
|
|
1593
|
+
const attr = fields[field];
|
|
1594
|
+
if (attr.references) {
|
|
1595
|
+
const referencedOriginalModel = attr.references.model;
|
|
1596
|
+
const referencedModelNameCap = capitalizeFirstLetter(getModelName(tables[referencedOriginalModel]?.modelName || referencedOriginalModel));
|
|
1597
|
+
if (!manyToManyRelations.has(referencedModelNameCap)) manyToManyRelations.set(referencedModelNameCap, /* @__PURE__ */ new Set());
|
|
1598
|
+
const currentModelNameCap = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
|
|
1599
|
+
manyToManyRelations.get(referencedModelNameCap).add(currentModelNameCap);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1495
1602
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1603
|
+
const indexedFields = /* @__PURE__ */ new Map();
|
|
1604
|
+
for (const table in tables) {
|
|
1605
|
+
const fields = tables[table]?.fields;
|
|
1606
|
+
const modelName = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
|
|
1607
|
+
indexedFields.set(modelName, []);
|
|
1608
|
+
for (const field in fields) {
|
|
1609
|
+
const attr = fields[field];
|
|
1610
|
+
if (attr.index && !attr.unique) {
|
|
1611
|
+
const fieldName = attr.fieldName || field;
|
|
1612
|
+
indexedFields.get(modelName).push(fieldName);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const schema = produceSchema(schemaPrisma, (builder) => {
|
|
1617
|
+
for (const table in tables) {
|
|
1618
|
+
const originalTableName = table;
|
|
1619
|
+
const customModelName = tables[table]?.modelName || table;
|
|
1620
|
+
const modelName = capitalizeFirstLetter(getModelName(customModelName));
|
|
1621
|
+
const fields = tables[table]?.fields;
|
|
1622
|
+
function getType({ isBigint, isOptional, type }) {
|
|
1623
|
+
if (type === "string") return isOptional ? "String?" : "String";
|
|
1624
|
+
if (type === "number" && isBigint) return isOptional ? "BigInt?" : "BigInt";
|
|
1625
|
+
if (type === "number") return isOptional ? "Int?" : "Int";
|
|
1626
|
+
if (type === "boolean") return isOptional ? "Boolean?" : "Boolean";
|
|
1627
|
+
if (type === "date") return isOptional ? "DateTime?" : "DateTime";
|
|
1628
|
+
if (type === "json") {
|
|
1629
|
+
if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String";
|
|
1630
|
+
return isOptional ? "Json?" : "Json";
|
|
1631
|
+
}
|
|
1632
|
+
if (type === "string[]") {
|
|
1633
|
+
if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String";
|
|
1634
|
+
return "String[]";
|
|
1635
|
+
}
|
|
1636
|
+
if (type === "number[]") {
|
|
1637
|
+
if (provider === "sqlite" || provider === "mysql") return "String";
|
|
1638
|
+
return "Int[]";
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const prismaModel = builder.findByType("model", { name: modelName });
|
|
1642
|
+
if (!prismaModel) if (provider === "mongodb") builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`);
|
|
1643
|
+
else {
|
|
1644
|
+
const useNumberId = options.advanced?.database?.generateId === "serial";
|
|
1645
|
+
const useUUIDs = options.advanced?.database?.generateId === "uuid";
|
|
1646
|
+
if (useNumberId) builder.model(modelName).field("id", "Int").attribute("id").attribute("default(autoincrement())");
|
|
1647
|
+
else if (useUUIDs && provider === "postgresql") builder.model(modelName).field("id", "String").attribute("id").attribute("default(dbgenerated(\"pg_catalog.gen_random_uuid()\"))").attribute("db.Uuid");
|
|
1648
|
+
else builder.model(modelName).field("id", "String").attribute("id");
|
|
1649
|
+
}
|
|
1650
|
+
for (const field in fields) {
|
|
1651
|
+
const attr = fields[field];
|
|
1652
|
+
const fieldName = attr.fieldName || field;
|
|
1653
|
+
if (prismaModel) {
|
|
1654
|
+
if (builder.findByType("field", {
|
|
1655
|
+
name: fieldName,
|
|
1656
|
+
within: prismaModel.properties
|
|
1657
|
+
})) continue;
|
|
1658
|
+
}
|
|
1659
|
+
const useUUIDs = options.advanced?.database?.generateId === "uuid";
|
|
1660
|
+
const useNumberId = options.advanced?.database?.generateId === "serial";
|
|
1661
|
+
const fieldBuilder = builder.model(modelName).field(fieldName, field === "id" && useNumberId ? getType({
|
|
1662
|
+
isBigint: false,
|
|
1663
|
+
isOptional: false,
|
|
1664
|
+
type: "number"
|
|
1665
|
+
}) : getType({
|
|
1666
|
+
isBigint: attr?.bigint || false,
|
|
1667
|
+
isOptional: attr?.required === false,
|
|
1668
|
+
type: attr.references?.field === "id" ? useNumberId ? "number" : "string" : attr.type
|
|
1669
|
+
}));
|
|
1670
|
+
if (field === "id") {
|
|
1671
|
+
fieldBuilder.attribute("id");
|
|
1672
|
+
if (provider === "mongodb") fieldBuilder.attribute(`map("_id")`);
|
|
1673
|
+
}
|
|
1674
|
+
if (attr.unique) builder.model(modelName).blockAttribute(`unique([${fieldName}])`);
|
|
1675
|
+
if (attr.defaultValue !== void 0) {
|
|
1676
|
+
if (Array.isArray(attr.defaultValue)) {
|
|
1677
|
+
if (attr.type === "json") {
|
|
1678
|
+
if (Object.prototype.toString.call(attr.defaultValue[0]) === "[object Object]") {
|
|
1679
|
+
fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`);
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
const jsonArray = [];
|
|
1683
|
+
for (const value of attr.defaultValue) jsonArray.push(value);
|
|
1684
|
+
fieldBuilder.attribute(`default("${JSON.stringify(jsonArray).replace(/"/g, "\\\"")}")`);
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (attr.defaultValue.length === 0) {
|
|
1688
|
+
fieldBuilder.attribute(`default([])`);
|
|
1689
|
+
continue;
|
|
1690
|
+
} else if (typeof attr.defaultValue[0] === "string" && attr.type === "string[]") {
|
|
1691
|
+
const valueArray = [];
|
|
1692
|
+
for (const value of attr.defaultValue) valueArray.push(JSON.stringify(value));
|
|
1693
|
+
fieldBuilder.attribute(`default([${valueArray}])`);
|
|
1694
|
+
} else if (typeof attr.defaultValue[0] === "number") {
|
|
1695
|
+
const valueArray = [];
|
|
1696
|
+
for (const value of attr.defaultValue) valueArray.push(`${value}`);
|
|
1697
|
+
fieldBuilder.attribute(`default([${valueArray}])`);
|
|
1698
|
+
}
|
|
1699
|
+
} else if (typeof attr.defaultValue === "object" && !Array.isArray(attr.defaultValue) && attr.defaultValue !== null) {
|
|
1700
|
+
if (Object.entries(attr.defaultValue).length === 0) {
|
|
1701
|
+
fieldBuilder.attribute(`default("{}")`);
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`);
|
|
1705
|
+
}
|
|
1706
|
+
if (field === "createdAt") fieldBuilder.attribute("default(now())");
|
|
1707
|
+
else if (typeof attr.defaultValue === "string" && provider !== "mysql") fieldBuilder.attribute(`default("${attr.defaultValue}")`);
|
|
1708
|
+
else if (typeof attr.defaultValue === "boolean" || typeof attr.defaultValue === "number") fieldBuilder.attribute(`default(${attr.defaultValue})`);
|
|
1709
|
+
else if (typeof attr.defaultValue === "function") {}
|
|
1710
|
+
}
|
|
1711
|
+
if (field === "updatedAt" && attr.onUpdate) fieldBuilder.attribute("updatedAt");
|
|
1712
|
+
else if (attr.onUpdate) {}
|
|
1713
|
+
if (attr.references) {
|
|
1714
|
+
if (useUUIDs && provider === "postgresql" && attr.references?.field === "id") builder.model(modelName).field(fieldName).attribute(`db.Uuid`);
|
|
1715
|
+
const referencedOriginalModelName = getModelName(attr.references.model);
|
|
1716
|
+
const referencedCustomModelName = tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName;
|
|
1717
|
+
let action = "Cascade";
|
|
1718
|
+
if (attr.references.onDelete === "no action") action = "NoAction";
|
|
1719
|
+
else if (attr.references.onDelete === "set null") action = "SetNull";
|
|
1720
|
+
else if (attr.references.onDelete === "set default") action = "SetDefault";
|
|
1721
|
+
else if (attr.references.onDelete === "restrict") action = "Restrict";
|
|
1722
|
+
const relationField = `relation(fields: [${getFieldName({
|
|
1723
|
+
model: originalTableName,
|
|
1724
|
+
field: fieldName
|
|
1725
|
+
})}], references: [${getFieldName({
|
|
1726
|
+
model: attr.references.model,
|
|
1727
|
+
field: attr.references.field
|
|
1728
|
+
})}], onDelete: ${action})`;
|
|
1729
|
+
builder.model(modelName).field(referencedCustomModelName.toLowerCase(), `${capitalizeFirstLetter(referencedCustomModelName)}${attr.required === false ? "?" : ""}`).attribute(relationField);
|
|
1730
|
+
}
|
|
1731
|
+
if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") builder.model(modelName).field(fieldName).attribute("db.Text");
|
|
1732
|
+
}
|
|
1733
|
+
if (manyToManyRelations.has(modelName)) for (const relatedModel of manyToManyRelations.get(modelName)) {
|
|
1734
|
+
const relatedTableName = Object.keys(tables).find((key) => capitalizeFirstLetter(tables[key]?.modelName || key) === relatedModel);
|
|
1735
|
+
const relatedFields = relatedTableName ? tables[relatedTableName]?.fields : {};
|
|
1736
|
+
const [_fieldKey, fkFieldAttr] = Object.entries(relatedFields || {}).find(([_fieldName, fieldAttr]) => fieldAttr.references && getModelName(fieldAttr.references.model) === getModelName(originalTableName)) || [];
|
|
1737
|
+
const isUnique = fkFieldAttr?.unique === true;
|
|
1738
|
+
const fieldName = isUnique || adapter.options?.usePlural === true ? `${relatedModel.toLowerCase()}` : `${relatedModel.toLowerCase()}s`;
|
|
1739
|
+
if (!builder.findByType("field", {
|
|
1740
|
+
name: fieldName,
|
|
1741
|
+
within: prismaModel?.properties
|
|
1742
|
+
})) builder.model(modelName).field(fieldName, `${relatedModel}${isUnique ? "?" : "[]"}`);
|
|
1743
|
+
}
|
|
1744
|
+
const indexedFieldsForModel = indexedFields.get(modelName);
|
|
1745
|
+
if (indexedFieldsForModel && indexedFieldsForModel.length > 0) for (const fieldName of indexedFieldsForModel) {
|
|
1746
|
+
if (prismaModel) {
|
|
1747
|
+
if (prismaModel.properties.some((v) => v.type === "attribute" && v.name === "index" && JSON.stringify(v.args[0]?.value).includes(fieldName))) continue;
|
|
1748
|
+
}
|
|
1749
|
+
const field = Object.entries(fields).find(([key, attr]) => (attr.fieldName || key) === fieldName)?.[1];
|
|
1750
|
+
let indexField = fieldName;
|
|
1751
|
+
if (provider === "mysql" && field && field.type === "string") {
|
|
1752
|
+
const useNumberId = options.advanced?.database?.generateId === "serial";
|
|
1753
|
+
const useUUIDs = options.advanced?.database?.generateId === "uuid";
|
|
1754
|
+
if (field.references?.field === "id" && (useNumberId || useUUIDs)) indexField = `${fieldName}`;
|
|
1755
|
+
else indexField = `${fieldName}(length: 191)`;
|
|
1756
|
+
}
|
|
1757
|
+
builder.model(modelName).blockAttribute(`index([${indexField}])`);
|
|
1758
|
+
}
|
|
1759
|
+
const hasAttribute = builder.findByType("attribute", {
|
|
1760
|
+
name: "map",
|
|
1761
|
+
within: prismaModel?.properties
|
|
1513
1762
|
});
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
}
|
|
1517
|
-
result.push(...collectReferencedTsconfigs(refTsconfigPath, visited));
|
|
1518
|
-
}
|
|
1519
|
-
return result;
|
|
1520
|
-
}
|
|
1521
|
-
/**
|
|
1522
|
-
* Ordered `paths` matchers from the project tsconfig and any referenced
|
|
1523
|
-
* tsconfigs, following TypeScript canonical resolution semantics.
|
|
1524
|
-
* @see https://github.com/microsoft/TypeScript/blob/main/src/compiler/moduleNameResolver.ts
|
|
1525
|
-
*/
|
|
1526
|
-
function collectPathsMatchers(cwd) {
|
|
1527
|
-
const tsconfig = getTsconfig(cwd, fs.existsSync(path.join(cwd, "tsconfig.json")) ? "tsconfig.json" : "jsconfig.json");
|
|
1528
|
-
if (!tsconfig) return [];
|
|
1529
|
-
const matchers = [];
|
|
1530
|
-
try {
|
|
1531
|
-
const mainMatcher = createPathsMatcher(tsconfig);
|
|
1532
|
-
if (mainMatcher) matchers.push(mainMatcher);
|
|
1533
|
-
for (const refTsconfig of collectReferencedTsconfigs(tsconfig.path)) {
|
|
1534
|
-
const refMatcher = createPathsMatcher(refTsconfig);
|
|
1535
|
-
if (refMatcher) matchers.push(refMatcher);
|
|
1763
|
+
const hasChanged = customModelName !== originalTableName;
|
|
1764
|
+
if (!hasAttribute) builder.model(modelName).blockAttribute("map", `${getModelName(hasChanged ? customModelName : originalTableName)}`);
|
|
1536
1765
|
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
throw new BetterAuthError("Error parsing tsconfig.json");
|
|
1540
|
-
}
|
|
1541
|
-
return matchers;
|
|
1542
|
-
}
|
|
1543
|
-
/**
|
|
1544
|
-
* Source file extensions jiti can load. Shared between the jiti `extensions`
|
|
1545
|
-
* option and `resolveCandidateFile` so both stay in sync.
|
|
1546
|
-
*/
|
|
1547
|
-
const SOURCE_EXTENSIONS = [
|
|
1548
|
-
".ts",
|
|
1549
|
-
".tsx",
|
|
1550
|
-
".mts",
|
|
1551
|
-
".cts",
|
|
1552
|
-
".js",
|
|
1553
|
-
".jsx",
|
|
1554
|
-
".mjs",
|
|
1555
|
-
".cjs"
|
|
1556
|
-
];
|
|
1557
|
-
const SOURCE_EXTENSIONS_SET = new Set(SOURCE_EXTENSIONS);
|
|
1558
|
-
/** Probes a candidate as-is, with known extensions, and as a directory index. */
|
|
1559
|
-
function resolveCandidateFile(candidate) {
|
|
1560
|
-
try {
|
|
1561
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
1562
|
-
} catch {}
|
|
1563
|
-
if (SOURCE_EXTENSIONS_SET.has(path.extname(candidate))) return;
|
|
1564
|
-
for (const ext of SOURCE_EXTENSIONS) {
|
|
1565
|
-
const withExt = candidate + ext;
|
|
1566
|
-
if (fs.existsSync(withExt)) return withExt;
|
|
1567
|
-
}
|
|
1568
|
-
for (const ext of SOURCE_EXTENSIONS) {
|
|
1569
|
-
const asIndex = path.join(candidate, `index${ext}`);
|
|
1570
|
-
if (fs.existsSync(asIndex)) return asIndex;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
function resolveWithMatchers(specifier, matchers) {
|
|
1574
|
-
for (const matcher of matchers) for (const candidate of matcher(specifier)) {
|
|
1575
|
-
const resolved = resolveCandidateFile(candidate);
|
|
1576
|
-
if (resolved) return resolved;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
/**
|
|
1580
|
-
* Callees whose first string argument is a module specifier. `jitiImport` is
|
|
1581
|
-
* a jiti-side preprocessor artifact observed in the AST; revisit on jiti
|
|
1582
|
-
* major version bumps (the regression suite catches a rename but not the why).
|
|
1583
|
-
*/
|
|
1584
|
-
const LOADER_IDENTIFIERS = new Set([
|
|
1585
|
-
"require",
|
|
1586
|
-
"import",
|
|
1587
|
-
"jitiImport"
|
|
1588
|
-
]);
|
|
1589
|
-
/**
|
|
1590
|
-
* Rewrites aliased specifiers at AST level. Required because jiti's `alias`
|
|
1591
|
-
* option only supports prefix matching and cannot express mid-path wildcards.
|
|
1592
|
-
*
|
|
1593
|
-
* Matchers always take precedence over native resolution, mirroring
|
|
1594
|
-
* TypeScript's own `paths` → `node_modules` order.
|
|
1595
|
-
*/
|
|
1596
|
-
function createRewriteImportPathsPlugin(matchers) {
|
|
1597
|
-
return ({ types: t }) => {
|
|
1598
|
-
const rewrite = (source) => {
|
|
1599
|
-
if (!source) return;
|
|
1600
|
-
const resolved = resolveWithMatchers(source.value, matchers);
|
|
1601
|
-
if (resolved) source.value = resolved;
|
|
1602
|
-
};
|
|
1603
|
-
return { visitor: {
|
|
1604
|
-
ImportDeclaration(p) {
|
|
1605
|
-
rewrite(p.node.source);
|
|
1606
|
-
},
|
|
1607
|
-
ExportNamedDeclaration(p) {
|
|
1608
|
-
rewrite(p.node.source);
|
|
1609
|
-
},
|
|
1610
|
-
ExportAllDeclaration(p) {
|
|
1611
|
-
rewrite(p.node.source);
|
|
1612
|
-
},
|
|
1613
|
-
ImportExpression(p) {
|
|
1614
|
-
if (t.isStringLiteral(p.node.source)) rewrite(p.node.source);
|
|
1615
|
-
},
|
|
1616
|
-
CallExpression(p) {
|
|
1617
|
-
const { callee, arguments: args } = p.node;
|
|
1618
|
-
const first = args[0];
|
|
1619
|
-
if (!t.isStringLiteral(first)) return;
|
|
1620
|
-
if (!(t.isIdentifier(callee) && LOADER_IDENTIFIERS.has(callee.name) || t.isImport(callee))) return;
|
|
1621
|
-
rewrite(first);
|
|
1622
|
-
}
|
|
1623
|
-
} };
|
|
1624
|
-
};
|
|
1625
|
-
}
|
|
1626
|
-
/** Virtual module aliases; real tsconfig paths go through the babel plugin. */
|
|
1627
|
-
function getVirtualModuleAliases() {
|
|
1628
|
-
const result = {};
|
|
1629
|
-
addSvelteKitEnvModules(result);
|
|
1630
|
-
addCloudflareModules(result);
|
|
1631
|
-
return result;
|
|
1632
|
-
}
|
|
1633
|
-
/**
|
|
1634
|
-
* .tsx files are not supported by Jiti.
|
|
1635
|
-
*/
|
|
1636
|
-
const jitiOptions = (cwd) => {
|
|
1637
|
-
const matchers = collectPathsMatchers(cwd);
|
|
1638
|
-
const plugins = matchers.length > 0 ? [createRewriteImportPathsPlugin(matchers)] : [];
|
|
1766
|
+
});
|
|
1767
|
+
const schemaChanged = schema.trim() !== schemaPrisma.trim();
|
|
1639
1768
|
return {
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
allExtensions: true
|
|
1644
|
-
}], [babelPresetReact, { runtime: "automatic" }]],
|
|
1645
|
-
plugins
|
|
1646
|
-
} },
|
|
1647
|
-
extensions: [...SOURCE_EXTENSIONS],
|
|
1648
|
-
alias: getVirtualModuleAliases()
|
|
1769
|
+
code: schemaChanged ? schema : "",
|
|
1770
|
+
fileName: filePath,
|
|
1771
|
+
overwrite: schemaPrismaExist && schemaChanged
|
|
1649
1772
|
};
|
|
1650
1773
|
};
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1774
|
+
const getNewPrisma = (provider, cwd) => {
|
|
1775
|
+
const prismaVersion = getPrismaVersion(cwd);
|
|
1776
|
+
const isV7 = prismaVersion && prismaVersion >= 7;
|
|
1777
|
+
const clientProvider = isV7 ? "prisma-client" : "prisma-client-js";
|
|
1778
|
+
if (isV7) return `generator client {
|
|
1779
|
+
provider = "${clientProvider}"
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
datasource db {
|
|
1783
|
+
provider = "${provider}"
|
|
1784
|
+
}`;
|
|
1785
|
+
return `generator client {
|
|
1786
|
+
provider = "${clientProvider}"
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
datasource db {
|
|
1790
|
+
provider = "${provider}"
|
|
1791
|
+
url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`}
|
|
1792
|
+
}`;
|
|
1793
|
+
};
|
|
1794
|
+
//#endregion
|
|
1795
|
+
//#region src/generators/index.ts
|
|
1796
|
+
const adapters = {
|
|
1797
|
+
prisma: generatePrismaSchema,
|
|
1798
|
+
drizzle: generateDrizzleSchema,
|
|
1799
|
+
kysely: generateKyselySchema
|
|
1800
|
+
};
|
|
1801
|
+
const generateSchema = (opts) => {
|
|
1802
|
+
const adapter = opts.adapter;
|
|
1803
|
+
const generator = adapter.id in adapters ? adapters[adapter.id] : null;
|
|
1804
|
+
if (generator) return generator(opts);
|
|
1805
|
+
if (adapter.createSchema) return adapter.createSchema(opts.options, opts.file).then(({ code, path: fileName, overwrite }) => ({
|
|
1806
|
+
code,
|
|
1807
|
+
fileName,
|
|
1808
|
+
overwrite
|
|
1809
|
+
}));
|
|
1810
|
+
throw new Error(`${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`);
|
|
1659
1811
|
};
|
|
1660
|
-
const isServerOnlyError = (e) => typeof e === "object" && e !== null && "message" in e && typeof e.message === "string" && e.message.includes("This module cannot be imported from a Client Component module");
|
|
1661
|
-
const SERVER_ONLY_HINT = `Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`;
|
|
1662
|
-
async function getConfig({ cwd, configPath, shouldThrowOnError = false }) {
|
|
1663
|
-
const fail = (message, error) => {
|
|
1664
|
-
if (shouldThrowOnError) throw error instanceof Error ? error : new Error(message);
|
|
1665
|
-
const log = `[#better-auth]: ${message}`;
|
|
1666
|
-
if (error) console.error(log, error);
|
|
1667
|
-
else console.error(log);
|
|
1668
|
-
process.exit(1);
|
|
1669
|
-
};
|
|
1670
|
-
const load = (configFile) => loadConfig({
|
|
1671
|
-
configFile,
|
|
1672
|
-
dotenv: { fileName: [".env", ".env.local"] },
|
|
1673
|
-
jitiOptions: jitiOptions(cwd),
|
|
1674
|
-
resolveModule: resolveAuthModule,
|
|
1675
|
-
cwd
|
|
1676
|
-
});
|
|
1677
|
-
try {
|
|
1678
|
-
if (configPath) {
|
|
1679
|
-
const resolvedPath = existsSync(configPath) ? configPath : path.join(cwd, configPath);
|
|
1680
|
-
const { config } = await load(resolvedPath);
|
|
1681
|
-
const options = config?.options;
|
|
1682
|
-
if (!options) return fail(`Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`);
|
|
1683
|
-
return options;
|
|
1684
|
-
}
|
|
1685
|
-
for (const possiblePath of possiblePaths$1) {
|
|
1686
|
-
let config;
|
|
1687
|
-
try {
|
|
1688
|
-
({config} = await load(possiblePath));
|
|
1689
|
-
} catch (e) {
|
|
1690
|
-
if (isServerOnlyError(e)) return fail(SERVER_ONLY_HINT);
|
|
1691
|
-
return fail("Couldn't read your auth config.", e);
|
|
1692
|
-
}
|
|
1693
|
-
if (Object.keys(config).length === 0) continue;
|
|
1694
|
-
if (!config.options) return fail("Couldn't read your auth config. Make sure to default export your auth instance or to export as a variable named auth.");
|
|
1695
|
-
return config.options;
|
|
1696
|
-
}
|
|
1697
|
-
return null;
|
|
1698
|
-
} catch (e) {
|
|
1699
|
-
if (isServerOnlyError(e)) return fail(SERVER_ONLY_HINT);
|
|
1700
|
-
return fail("Couldn't read your auth config.", e);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
1812
|
//#endregion
|
|
1704
1813
|
//#region src/commands/generate.ts
|
|
1705
1814
|
function createMockAdapter$1(adapterId, dialect) {
|
|
@@ -1736,6 +1845,9 @@ function createMockAdapter$1(adapterId, dialect) {
|
|
|
1736
1845
|
deleteMany: async () => {
|
|
1737
1846
|
throw new Error("Mock adapter methods should not be called");
|
|
1738
1847
|
},
|
|
1848
|
+
consumeOne: async () => {
|
|
1849
|
+
throw new Error("Mock adapter methods should not be called");
|
|
1850
|
+
},
|
|
1739
1851
|
transaction: async (callback) => {
|
|
1740
1852
|
throw new Error("Mock adapter methods should not be called");
|
|
1741
1853
|
},
|
|
@@ -6648,7 +6760,7 @@ const initActionOptionsSchema = z.object({
|
|
|
6648
6760
|
//#region src/commands/login.ts
|
|
6649
6761
|
async function loginAction() {
|
|
6650
6762
|
try {
|
|
6651
|
-
await spawnCommand("npx
|
|
6763
|
+
await spawnCommand("npx auth@latest login");
|
|
6652
6764
|
} catch (error) {
|
|
6653
6765
|
log.error(error.message || "An unknown error occurred");
|
|
6654
6766
|
process.exit(1);
|
|
@@ -6658,7 +6770,7 @@ async function loginAction() {
|
|
|
6658
6770
|
const login = new Command("login").description("Login to Better Auth Infrastructure").action(loginAction);
|
|
6659
6771
|
async function logoutAction() {
|
|
6660
6772
|
try {
|
|
6661
|
-
await spawnCommand("npx
|
|
6773
|
+
await spawnCommand("npx auth@latest logout");
|
|
6662
6774
|
} catch (error) {
|
|
6663
6775
|
log.error(error.message || "An unknown error occurred");
|
|
6664
6776
|
process.exit(1);
|
|
@@ -7060,7 +7172,7 @@ async function main() {
|
|
|
7060
7172
|
packageInfo = await getPackageInfo();
|
|
7061
7173
|
cliVersion = packageInfo.version || "1.1.2";
|
|
7062
7174
|
} catch {}
|
|
7063
|
-
program.addCommand(ai).addCommand(init).addCommand(migrate).addCommand(generate).addCommand(generateSecret).addCommand(info).addCommand(login).addCommand(logout).addCommand(mcp).addCommand(upgrade).version(cliVersion).description("Better Auth CLI").action(() => program.help());
|
|
7175
|
+
program.addCommand(ai).addCommand(createAdmin).addCommand(init).addCommand(migrate).addCommand(generate).addCommand(generateSecret).addCommand(info).addCommand(login).addCommand(logout).addCommand(mcp).addCommand(upgrade).version(cliVersion).description("Better Auth CLI").action(() => program.help());
|
|
7064
7176
|
program.parse();
|
|
7065
7177
|
}
|
|
7066
7178
|
main().catch((error) => {
|