@invect/cli 0.0.1
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/LICENSE +21 -0
- package/dist/api.js +42 -0
- package/dist/chunk-DGBTXQND.js +109 -0
- package/dist/chunk-K4RRNATQ.js +458 -0
- package/dist/chunk-Q6JKV7VX.js +157 -0
- package/dist/chunk-T4KEEHFJ.js +569 -0
- package/dist/generate-JDAYY3OL.js +10 -0
- package/dist/index.js +581 -0
- package/dist/migrate-JMKLRLSJ.js +9 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 @robase
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateAllDrizzleSchemas,
|
|
3
|
+
generateAppendSchema,
|
|
4
|
+
generateDrizzleSchema,
|
|
5
|
+
generatePrismaSchema
|
|
6
|
+
} from "./chunk-K4RRNATQ.js";
|
|
7
|
+
|
|
8
|
+
// src/generators/index.ts
|
|
9
|
+
var adapters = {
|
|
10
|
+
drizzle: generateDrizzleSchema,
|
|
11
|
+
prisma: generatePrismaSchema
|
|
12
|
+
};
|
|
13
|
+
async function generateSchema(opts) {
|
|
14
|
+
if (opts.adapter === "prisma") {
|
|
15
|
+
return generatePrismaSchema({
|
|
16
|
+
plugins: opts.plugins,
|
|
17
|
+
file: opts.file,
|
|
18
|
+
provider: opts.provider
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (opts.adapter === "drizzle") {
|
|
22
|
+
if (!opts.dialect) {
|
|
23
|
+
throw new Error("dialect is required for the drizzle adapter");
|
|
24
|
+
}
|
|
25
|
+
return generateDrizzleSchema({
|
|
26
|
+
plugins: opts.plugins,
|
|
27
|
+
file: opts.file,
|
|
28
|
+
dialect: opts.dialect
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Unsupported adapter "${opts.adapter}". Invect supports: ${Object.keys(adapters).join(", ")}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
adapters,
|
|
37
|
+
generateAllDrizzleSchemas,
|
|
38
|
+
generateAppendSchema,
|
|
39
|
+
generateDrizzleSchema,
|
|
40
|
+
generatePrismaSchema,
|
|
41
|
+
generateSchema
|
|
42
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findConfigPath,
|
|
3
|
+
loadConfig
|
|
4
|
+
} from "./chunk-Q6JKV7VX.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/migrate.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
import prompts from "prompts";
|
|
12
|
+
import { execSync } from "child_process";
|
|
13
|
+
var migrateCommand = new Command("migrate").description("Apply pending database migrations via Drizzle Kit").option("--config <path>", "Path to your Invect config file").option("-y, --yes", "Skip confirmation prompt").option("--push", "Push schema directly without migration files (dev mode)").action(migrateAction);
|
|
14
|
+
async function migrateAction(options) {
|
|
15
|
+
console.log(pc.bold("\n\u{1F5C4}\uFE0F Invect Migration\n"));
|
|
16
|
+
const configPath = findConfigPath(options.config);
|
|
17
|
+
if (!configPath) {
|
|
18
|
+
console.error(
|
|
19
|
+
pc.red("\u2717 Could not find Invect config file.") + "\n" + pc.dim(" Use --config <path> to specify the config file explicitly.") + "\n\n" + pc.dim(" You can create one with: " + pc.cyan("npx invect init")) + "\n"
|
|
20
|
+
);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
console.log(pc.dim(` Config: ${path.relative(process.cwd(), configPath)}`));
|
|
24
|
+
let config;
|
|
25
|
+
try {
|
|
26
|
+
config = await loadConfig(configPath);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(pc.red(`\u2717 ${error instanceof Error ? error.message : String(error)}`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const dbType = config.baseDatabaseConfig?.type;
|
|
32
|
+
const dbUrl = config.baseDatabaseConfig?.connectionString;
|
|
33
|
+
if (!dbType) {
|
|
34
|
+
console.error(
|
|
35
|
+
pc.red("\u2717 No baseDatabaseConfig.type found in your config.") + "\n" + pc.dim(
|
|
36
|
+
' Expected: baseDatabaseConfig: { type: "sqlite" | "postgresql" | "mysql", ... }'
|
|
37
|
+
) + "\n"
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
console.log(pc.dim(` Database: ${dbType}`));
|
|
42
|
+
if (dbUrl) {
|
|
43
|
+
const redacted = dbUrl.replace(/:\/\/[^@]+@/, "://***@");
|
|
44
|
+
console.log(pc.dim(` Connection: ${redacted}`));
|
|
45
|
+
}
|
|
46
|
+
const mode = options.push ? "push" : "migrate";
|
|
47
|
+
console.log(pc.dim(` Mode: ${mode === "push" ? "push (direct schema sync)" : "migrate (SQL migration files)"}`));
|
|
48
|
+
if (!options.yes) {
|
|
49
|
+
const message = mode === "push" ? `Push schema directly to your ${dbType} database?` : `Apply pending migrations to your ${dbType} database?`;
|
|
50
|
+
const response = await prompts({
|
|
51
|
+
type: "confirm",
|
|
52
|
+
name: "proceed",
|
|
53
|
+
message,
|
|
54
|
+
initial: true
|
|
55
|
+
});
|
|
56
|
+
if (!response.proceed) {
|
|
57
|
+
console.log(pc.dim("\n Cancelled.\n"));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log(pc.dim(`
|
|
62
|
+
Running drizzle-kit ${mode}...
|
|
63
|
+
`));
|
|
64
|
+
try {
|
|
65
|
+
const drizzleConfigFile = detectDrizzleConfig(dbType);
|
|
66
|
+
const configFlag = drizzleConfigFile ? ` --config ${drizzleConfigFile}` : "";
|
|
67
|
+
const cmd = `npx drizzle-kit ${mode}${configFlag}`;
|
|
68
|
+
execSync(cmd, {
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
cwd: process.cwd()
|
|
71
|
+
});
|
|
72
|
+
if (mode === "push") {
|
|
73
|
+
console.log(pc.bold(pc.green("\n\u2713 Schema pushed successfully!\n")));
|
|
74
|
+
} else {
|
|
75
|
+
console.log(pc.bold(pc.green("\n\u2713 Migrations applied successfully!\n")));
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
console.error(pc.red(`
|
|
79
|
+
\u2717 drizzle-kit ${mode} failed.`));
|
|
80
|
+
console.error(
|
|
81
|
+
pc.dim(" Make sure drizzle-kit is installed and your drizzle config is correct.\n")
|
|
82
|
+
);
|
|
83
|
+
if (mode === "migrate") {
|
|
84
|
+
console.error(
|
|
85
|
+
pc.dim(" Have you generated migrations? Run: ") + pc.cyan("npx invect generate") + "\n"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function detectDrizzleConfig(dbType) {
|
|
92
|
+
const candidates = {
|
|
93
|
+
sqlite: ["drizzle.config.sqlite.ts", "drizzle.config.ts"],
|
|
94
|
+
postgresql: ["drizzle.config.postgres.ts", "drizzle.config.postgresql.ts", "drizzle.config.ts"],
|
|
95
|
+
mysql: ["drizzle.config.mysql.ts", "drizzle.config.ts"]
|
|
96
|
+
};
|
|
97
|
+
const searchPaths = candidates[dbType] || ["drizzle.config.ts"];
|
|
98
|
+
for (const filename of searchPaths) {
|
|
99
|
+
if (fs.existsSync(path.resolve(process.cwd(), filename))) {
|
|
100
|
+
return filename;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
migrateCommand,
|
|
108
|
+
migrateAction
|
|
109
|
+
};
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// src/generators/drizzle.ts
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
var generateDrizzleSchema = async ({
|
|
4
|
+
plugins,
|
|
5
|
+
file,
|
|
6
|
+
dialect
|
|
7
|
+
}) => {
|
|
8
|
+
const {
|
|
9
|
+
mergeSchemas,
|
|
10
|
+
generateSqliteSchema,
|
|
11
|
+
generatePostgresSchema,
|
|
12
|
+
generateMysqlSchema
|
|
13
|
+
} = await import("@invect/core");
|
|
14
|
+
const mergedSchema = mergeSchemas(plugins);
|
|
15
|
+
const generators = {
|
|
16
|
+
sqlite: generateSqliteSchema,
|
|
17
|
+
postgresql: generatePostgresSchema,
|
|
18
|
+
mysql: generateMysqlSchema
|
|
19
|
+
};
|
|
20
|
+
const generator = generators[dialect];
|
|
21
|
+
if (!generator) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Unsupported dialect "${dialect}". Expected one of: sqlite, postgresql, mysql`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const code = generator(mergedSchema);
|
|
27
|
+
const defaultFiles = {
|
|
28
|
+
sqlite: "./src/database/schema-sqlite.ts",
|
|
29
|
+
postgresql: "./src/database/schema-postgres.ts",
|
|
30
|
+
mysql: "./src/database/schema-mysql.ts"
|
|
31
|
+
};
|
|
32
|
+
const fileName = file || defaultFiles[dialect];
|
|
33
|
+
if (existsSync(fileName)) {
|
|
34
|
+
const existing = readFileSync(fileName, "utf-8");
|
|
35
|
+
if (existing === code) {
|
|
36
|
+
return { code: void 0, fileName };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { code, fileName, overwrite: existsSync(fileName) };
|
|
40
|
+
};
|
|
41
|
+
async function generateAllDrizzleSchemas(options) {
|
|
42
|
+
const {
|
|
43
|
+
mergeSchemas,
|
|
44
|
+
CORE_SCHEMA
|
|
45
|
+
} = await import("@invect/core");
|
|
46
|
+
const mergedSchema = mergeSchemas(options.plugins);
|
|
47
|
+
const coreTableCount = Object.keys(CORE_SCHEMA).length;
|
|
48
|
+
const pluginsWithSchema = options.plugins.filter(
|
|
49
|
+
(p) => p.schema
|
|
50
|
+
).length;
|
|
51
|
+
const {
|
|
52
|
+
generateSqliteSchema,
|
|
53
|
+
generatePostgresSchema,
|
|
54
|
+
generateMysqlSchema
|
|
55
|
+
} = await import("@invect/core");
|
|
56
|
+
const dir = options.outputDir || "./src/database";
|
|
57
|
+
const dialects = [
|
|
58
|
+
{ dialect: "sqlite", fileName: `${dir}/schema-sqlite.ts`, generate: generateSqliteSchema },
|
|
59
|
+
{ dialect: "postgresql", fileName: `${dir}/schema-postgres.ts`, generate: generatePostgresSchema },
|
|
60
|
+
{ dialect: "mysql", fileName: `${dir}/schema-mysql.ts`, generate: generateMysqlSchema }
|
|
61
|
+
];
|
|
62
|
+
const results = [];
|
|
63
|
+
for (const { fileName, generate } of dialects) {
|
|
64
|
+
const code = generate(mergedSchema);
|
|
65
|
+
const exists = existsSync(fileName);
|
|
66
|
+
if (exists) {
|
|
67
|
+
const existing = readFileSync(fileName, "utf-8");
|
|
68
|
+
if (existing === code) {
|
|
69
|
+
results.push({ code: void 0, fileName });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
results.push({ code, fileName, overwrite: exists });
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
results,
|
|
77
|
+
mergedSchema,
|
|
78
|
+
stats: {
|
|
79
|
+
totalTables: mergedSchema.tables.length,
|
|
80
|
+
coreTableCount,
|
|
81
|
+
pluginTableCount: mergedSchema.tables.length - coreTableCount,
|
|
82
|
+
pluginsWithSchema
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function generateAppendSchema(options) {
|
|
87
|
+
const {
|
|
88
|
+
mergeSchemas,
|
|
89
|
+
CORE_SCHEMA,
|
|
90
|
+
generateSqliteSchemaAppend,
|
|
91
|
+
generatePostgresSchemaAppend,
|
|
92
|
+
generateMysqlSchemaAppend
|
|
93
|
+
} = await import("@invect/core");
|
|
94
|
+
const mergedSchema = mergeSchemas(options.plugins);
|
|
95
|
+
const coreTableCount = Object.keys(CORE_SCHEMA).length;
|
|
96
|
+
const generators = {
|
|
97
|
+
sqlite: generateSqliteSchemaAppend,
|
|
98
|
+
postgresql: generatePostgresSchemaAppend,
|
|
99
|
+
mysql: generateMysqlSchemaAppend
|
|
100
|
+
};
|
|
101
|
+
const generator = generators[options.dialect];
|
|
102
|
+
if (!generator) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Unsupported dialect "${options.dialect}". Expected one of: sqlite, postgresql, mysql`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const result = generator(mergedSchema);
|
|
108
|
+
return {
|
|
109
|
+
result,
|
|
110
|
+
stats: {
|
|
111
|
+
totalTables: mergedSchema.tables.length,
|
|
112
|
+
coreTableCount,
|
|
113
|
+
pluginTableCount: mergedSchema.tables.length - coreTableCount
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/generators/prisma.ts
|
|
119
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
120
|
+
import path from "path";
|
|
121
|
+
import { produceSchema } from "@mrleebo/prisma-ast";
|
|
122
|
+
function getPrismaVersion(cwd) {
|
|
123
|
+
try {
|
|
124
|
+
const pkgPath = path.join(cwd || process.cwd(), "package.json");
|
|
125
|
+
if (!existsSync2(pkgPath)) return null;
|
|
126
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
127
|
+
const version = pkg.dependencies?.prisma || pkg.devDependencies?.prisma || pkg.dependencies?.["@prisma/client"] || pkg.devDependencies?.["@prisma/client"];
|
|
128
|
+
if (!version) return null;
|
|
129
|
+
const match = version.match(/(\d+)/);
|
|
130
|
+
return match ? parseInt(match[1], 10) : null;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function generatePrismaSchema(options) {
|
|
136
|
+
const {
|
|
137
|
+
mergeSchemas
|
|
138
|
+
} = await import("@invect/core");
|
|
139
|
+
const mergedSchema = mergeSchemas(options.plugins);
|
|
140
|
+
const provider = options.provider || "postgresql";
|
|
141
|
+
const filePath = options.file || "./prisma/schema.prisma";
|
|
142
|
+
const fileExists = existsSync2(filePath);
|
|
143
|
+
let code;
|
|
144
|
+
if (fileExists) {
|
|
145
|
+
const existingContent = readFileSync2(filePath, "utf-8");
|
|
146
|
+
code = mergeIntoExistingSchema(existingContent, mergedSchema, provider);
|
|
147
|
+
} else {
|
|
148
|
+
const baseSchema = generateMinimalPrismaBase(provider);
|
|
149
|
+
code = mergeIntoExistingSchema(baseSchema, mergedSchema, provider);
|
|
150
|
+
}
|
|
151
|
+
if (fileExists) {
|
|
152
|
+
const existingContent = readFileSync2(filePath, "utf-8");
|
|
153
|
+
if (normalizeSchema(existingContent) === normalizeSchema(code)) {
|
|
154
|
+
return { code: void 0, fileName: filePath };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
code,
|
|
159
|
+
fileName: filePath,
|
|
160
|
+
overwrite: fileExists
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function normalizeSchema(schema) {
|
|
164
|
+
return schema.split("\n").map((line) => line.trimEnd()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
165
|
+
}
|
|
166
|
+
function generateMinimalPrismaBase(provider) {
|
|
167
|
+
const prismaVersion = getPrismaVersion();
|
|
168
|
+
const isV7 = prismaVersion !== null && prismaVersion >= 7;
|
|
169
|
+
const clientProvider = isV7 ? "prisma-client" : "prisma-client-js";
|
|
170
|
+
if (isV7) {
|
|
171
|
+
return `generator client {
|
|
172
|
+
provider = "${clientProvider}"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
datasource db {
|
|
176
|
+
provider = "${provider}"
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
const url = provider === "sqlite" ? '"file:./dev.db"' : 'env("DATABASE_URL")';
|
|
181
|
+
return `generator client {
|
|
182
|
+
provider = "${clientProvider}"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
datasource db {
|
|
186
|
+
provider = "${provider}"
|
|
187
|
+
url = ${url}
|
|
188
|
+
}
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
function mergeIntoExistingSchema(existingContent, mergedSchema, provider) {
|
|
192
|
+
const prismaVersion = getPrismaVersion();
|
|
193
|
+
const isV7 = prismaVersion !== null && prismaVersion >= 7;
|
|
194
|
+
let contentToMerge = existingContent;
|
|
195
|
+
if (isV7) {
|
|
196
|
+
contentToMerge = produceSchema(contentToMerge, (builder) => {
|
|
197
|
+
const generator = builder.findByType("generator", { name: "client" });
|
|
198
|
+
if (generator?.assignments) {
|
|
199
|
+
const providerProp = generator.assignments.find(
|
|
200
|
+
(prop) => prop.type === "assignment" && prop.key === "provider"
|
|
201
|
+
);
|
|
202
|
+
if (providerProp && providerProp.value === '"prisma-client-js"') {
|
|
203
|
+
providerProp.value = '"prisma-client"';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const datasource = builder.findByType("datasource", { name: "db" });
|
|
207
|
+
if (datasource?.assignments) {
|
|
208
|
+
const urlIndex = datasource.assignments.findIndex(
|
|
209
|
+
(prop) => prop.type === "assignment" && prop.key === "url"
|
|
210
|
+
);
|
|
211
|
+
if (urlIndex !== -1) {
|
|
212
|
+
datasource.assignments.splice(urlIndex, 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return produceSchema(contentToMerge, (builder) => {
|
|
218
|
+
for (const table of mergedSchema.tables) {
|
|
219
|
+
const modelName = capitalize(table.name);
|
|
220
|
+
const existingModel = builder.findByType("model", { name: modelName });
|
|
221
|
+
if (!existingModel) {
|
|
222
|
+
createPrismaModel(builder, table, mergedSchema, provider);
|
|
223
|
+
} else {
|
|
224
|
+
addMissingFields(builder, existingModel, table, mergedSchema, provider);
|
|
225
|
+
addTableMapping(builder, table, modelName, existingModel);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function createPrismaModel(builder, table, schema, provider) {
|
|
231
|
+
const modelName = capitalize(table.name);
|
|
232
|
+
const { definition } = table;
|
|
233
|
+
const uniqueFields = [];
|
|
234
|
+
for (const [fieldName, field] of Object.entries(definition.fields)) {
|
|
235
|
+
const prismaType = getPrismaType(field, provider);
|
|
236
|
+
const fieldBuilder = builder.model(modelName).field(fieldName, prismaType);
|
|
237
|
+
if (field.primaryKey) {
|
|
238
|
+
fieldBuilder.attribute("id");
|
|
239
|
+
}
|
|
240
|
+
addDefaultAttribute(fieldBuilder, field, fieldName, provider);
|
|
241
|
+
if (fieldName === "updatedAt" && field.type === "date") {
|
|
242
|
+
fieldBuilder.attribute("updatedAt");
|
|
243
|
+
}
|
|
244
|
+
const dbColName = toSnakeCase(fieldName);
|
|
245
|
+
if (dbColName !== fieldName) {
|
|
246
|
+
fieldBuilder.attribute(`map("${dbColName}")`);
|
|
247
|
+
}
|
|
248
|
+
if (provider === "mysql" && field.type === "text") {
|
|
249
|
+
fieldBuilder.attribute("db.Text");
|
|
250
|
+
}
|
|
251
|
+
if (provider === "postgresql" && field.type === "date") {
|
|
252
|
+
fieldBuilder.attribute("db.Timestamptz(3)");
|
|
253
|
+
}
|
|
254
|
+
if (field.unique) {
|
|
255
|
+
uniqueFields.push(fieldName);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const [fieldName, field] of Object.entries(definition.fields)) {
|
|
259
|
+
if (!field.references) continue;
|
|
260
|
+
const refLogical = findLogicalName(schema, field.references.table);
|
|
261
|
+
const refModel = capitalize(refLogical);
|
|
262
|
+
const relName = fieldName.replace(/Id$/, "").replace(/_id$/, "");
|
|
263
|
+
const onDelete = mapOnDelete(field.references.onDelete);
|
|
264
|
+
builder.model(modelName).field(relName, `${refModel}${!field.required ? "?" : ""}`).attribute(
|
|
265
|
+
`relation(fields: [${fieldName}], references: [${field.references.field}], onDelete: ${onDelete})`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
for (const otherTable of schema.tables) {
|
|
269
|
+
if (otherTable.name === table.name) continue;
|
|
270
|
+
for (const [_, field] of Object.entries(otherTable.definition.fields)) {
|
|
271
|
+
if (!field.references) continue;
|
|
272
|
+
const refsThisTable = field.references.table === table.name || field.references.table === (definition.tableName || toSnakeCase(table.name));
|
|
273
|
+
if (refsThisTable) {
|
|
274
|
+
const otherModelName = capitalize(otherTable.name);
|
|
275
|
+
const isUnique = field.unique === true;
|
|
276
|
+
builder.model(modelName).field(
|
|
277
|
+
otherTable.name,
|
|
278
|
+
`${otherModelName}${isUnique ? "?" : "[]"}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const fieldName of uniqueFields) {
|
|
284
|
+
builder.model(modelName).blockAttribute(`unique([${fieldName}])`);
|
|
285
|
+
}
|
|
286
|
+
for (const [fieldName, field] of Object.entries(definition.fields)) {
|
|
287
|
+
if (field.index && !field.unique && !field.primaryKey) {
|
|
288
|
+
builder.model(modelName).blockAttribute(`index([${fieldName}])`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (definition.compositePrimaryKey?.length) {
|
|
292
|
+
builder.model(modelName).blockAttribute("id", `[${definition.compositePrimaryKey.join(", ")}]`);
|
|
293
|
+
}
|
|
294
|
+
const dbTableName = definition.tableName || toSnakeCase(table.name);
|
|
295
|
+
if (dbTableName !== table.name) {
|
|
296
|
+
builder.model(modelName).blockAttribute("map", dbTableName);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function addMissingFields(builder, existingModel, table, schema, provider) {
|
|
300
|
+
const modelName = capitalize(table.name);
|
|
301
|
+
const { definition } = table;
|
|
302
|
+
const uniqueFields = [];
|
|
303
|
+
for (const [fieldName, field] of Object.entries(definition.fields)) {
|
|
304
|
+
const isAlreadyExist = builder.findByType("field", {
|
|
305
|
+
name: fieldName,
|
|
306
|
+
within: existingModel.properties
|
|
307
|
+
});
|
|
308
|
+
if (isAlreadyExist) continue;
|
|
309
|
+
const prismaType = getPrismaType(field, provider);
|
|
310
|
+
const fieldBuilder = builder.model(modelName).field(fieldName, prismaType);
|
|
311
|
+
if (field.primaryKey) {
|
|
312
|
+
fieldBuilder.attribute("id");
|
|
313
|
+
}
|
|
314
|
+
addDefaultAttribute(fieldBuilder, field, fieldName, provider);
|
|
315
|
+
if (fieldName === "updatedAt" && field.type === "date") {
|
|
316
|
+
fieldBuilder.attribute("updatedAt");
|
|
317
|
+
}
|
|
318
|
+
const dbColName = toSnakeCase(fieldName);
|
|
319
|
+
if (dbColName !== fieldName) {
|
|
320
|
+
fieldBuilder.attribute(`map("${dbColName}")`);
|
|
321
|
+
}
|
|
322
|
+
if (provider === "mysql" && field.type === "text") {
|
|
323
|
+
fieldBuilder.attribute("db.Text");
|
|
324
|
+
}
|
|
325
|
+
if (provider === "postgresql" && field.type === "date") {
|
|
326
|
+
fieldBuilder.attribute("db.Timestamptz(3)");
|
|
327
|
+
}
|
|
328
|
+
if (field.unique) {
|
|
329
|
+
uniqueFields.push(fieldName);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const [fieldName, field] of Object.entries(definition.fields)) {
|
|
333
|
+
if (!field.references) continue;
|
|
334
|
+
const isFieldAlreadyExist = builder.findByType("field", {
|
|
335
|
+
name: fieldName,
|
|
336
|
+
within: existingModel.properties
|
|
337
|
+
});
|
|
338
|
+
if (isFieldAlreadyExist) continue;
|
|
339
|
+
const refLogical = findLogicalName(schema, field.references.table);
|
|
340
|
+
const refModel = capitalize(refLogical);
|
|
341
|
+
const relName = fieldName.replace(/Id$/, "").replace(/_id$/, "");
|
|
342
|
+
const onDelete = mapOnDelete(field.references.onDelete);
|
|
343
|
+
const relExists = builder.findByType("field", {
|
|
344
|
+
name: relName,
|
|
345
|
+
within: existingModel.properties
|
|
346
|
+
});
|
|
347
|
+
if (!relExists) {
|
|
348
|
+
builder.model(modelName).field(relName, `${refModel}${!field.required ? "?" : ""}`).attribute(
|
|
349
|
+
`relation(fields: [${fieldName}], references: [${field.references.field}], onDelete: ${onDelete})`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
for (const fieldName of uniqueFields) {
|
|
354
|
+
builder.model(modelName).blockAttribute(`unique([${fieldName}])`);
|
|
355
|
+
}
|
|
356
|
+
for (const [fieldName, field] of Object.entries(definition.fields)) {
|
|
357
|
+
if (field.index && !field.unique && !field.primaryKey) {
|
|
358
|
+
const indexExists = existingModel.properties?.some(
|
|
359
|
+
(v) => v.type === "attribute" && v.name === "index" && JSON.stringify(v.args?.[0]?.value)?.includes(fieldName)
|
|
360
|
+
);
|
|
361
|
+
if (!indexExists) {
|
|
362
|
+
builder.model(modelName).blockAttribute(`index([${fieldName}])`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function addTableMapping(builder, table, modelName, existingModel) {
|
|
368
|
+
const dbTableName = table.definition.tableName || toSnakeCase(table.name);
|
|
369
|
+
if (dbTableName === table.name) return;
|
|
370
|
+
if (existingModel) {
|
|
371
|
+
const hasMap = builder.findByType("attribute", {
|
|
372
|
+
name: "map",
|
|
373
|
+
within: existingModel.properties
|
|
374
|
+
});
|
|
375
|
+
if (hasMap) return;
|
|
376
|
+
}
|
|
377
|
+
builder.model(modelName).blockAttribute("map", dbTableName);
|
|
378
|
+
}
|
|
379
|
+
function getPrismaType(field, provider) {
|
|
380
|
+
const optional = field.required === false ? "?" : "";
|
|
381
|
+
if (Array.isArray(field.type)) {
|
|
382
|
+
return `String${optional}`;
|
|
383
|
+
}
|
|
384
|
+
switch (field.type) {
|
|
385
|
+
case "string":
|
|
386
|
+
case "text":
|
|
387
|
+
case "uuid":
|
|
388
|
+
return `String${optional}`;
|
|
389
|
+
case "number":
|
|
390
|
+
return `Int${optional}`;
|
|
391
|
+
case "bigint":
|
|
392
|
+
return `BigInt${optional}`;
|
|
393
|
+
case "boolean":
|
|
394
|
+
return `Boolean${optional}`;
|
|
395
|
+
case "date":
|
|
396
|
+
return `DateTime${optional}`;
|
|
397
|
+
case "json":
|
|
398
|
+
if (provider === "sqlite") return `String${optional}`;
|
|
399
|
+
return `Json${optional}`;
|
|
400
|
+
default:
|
|
401
|
+
return `String${optional}`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function addDefaultAttribute(fieldBuilder, field, fieldName, _provider) {
|
|
405
|
+
if (field.defaultValue === void 0) return;
|
|
406
|
+
if (field.defaultValue === "uuid()") {
|
|
407
|
+
fieldBuilder.attribute("default(uuid())");
|
|
408
|
+
} else if (field.defaultValue === "now()") {
|
|
409
|
+
if (fieldName !== "updatedAt") {
|
|
410
|
+
fieldBuilder.attribute("default(now())");
|
|
411
|
+
}
|
|
412
|
+
} else if (typeof field.defaultValue === "boolean") {
|
|
413
|
+
fieldBuilder.attribute(`default(${field.defaultValue})`);
|
|
414
|
+
} else if (typeof field.defaultValue === "number") {
|
|
415
|
+
fieldBuilder.attribute(`default(${field.defaultValue})`);
|
|
416
|
+
} else if (typeof field.defaultValue === "string") {
|
|
417
|
+
fieldBuilder.attribute(`default("${field.defaultValue}")`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function mapOnDelete(onDelete) {
|
|
421
|
+
switch (onDelete) {
|
|
422
|
+
case "cascade":
|
|
423
|
+
return "Cascade";
|
|
424
|
+
case "set null":
|
|
425
|
+
return "SetNull";
|
|
426
|
+
case "restrict":
|
|
427
|
+
return "Restrict";
|
|
428
|
+
case "no action":
|
|
429
|
+
return "NoAction";
|
|
430
|
+
default:
|
|
431
|
+
return "NoAction";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function toSnakeCase(str) {
|
|
435
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
436
|
+
}
|
|
437
|
+
function capitalize(str) {
|
|
438
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
439
|
+
}
|
|
440
|
+
function findLogicalName(schema, tableRef) {
|
|
441
|
+
const exact = schema.tables.find((t) => t.name === tableRef);
|
|
442
|
+
if (exact) return exact.name;
|
|
443
|
+
const byDbName = schema.tables.find(
|
|
444
|
+
(t) => t.definition.tableName === tableRef || toSnakeCase(t.name) === tableRef
|
|
445
|
+
);
|
|
446
|
+
if (byDbName) return byDbName.name;
|
|
447
|
+
if (tableRef.includes("_")) {
|
|
448
|
+
return tableRef.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
449
|
+
}
|
|
450
|
+
return tableRef;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export {
|
|
454
|
+
generateDrizzleSchema,
|
|
455
|
+
generateAllDrizzleSchemas,
|
|
456
|
+
generateAppendSchema,
|
|
457
|
+
generatePrismaSchema
|
|
458
|
+
};
|