@zenstackhq/cli 3.5.5 → 3.6.0
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/bin/cli +1 -1
- package/dist/index.cjs +2800 -3570
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -2
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +3446 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +14 -13
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -4224
- package/dist/index.js.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3446 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { ZModelCodeGenerator, ZModelLanguageMetaData, formatDocument, loadDocument } from "@zenstackhq/language";
|
|
3
|
+
import colors from "colors";
|
|
4
|
+
import { Command, CommanderError, Option } from "commander";
|
|
5
|
+
import "dotenv/config";
|
|
6
|
+
import { DataModel, Enum, isDataSource, isEnum, isInvocationExpr, isLiteralExpr, isPlugin } from "@zenstackhq/language/ast";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { invariant, lowerCaseFirst, singleDebounce } from "@zenstackhq/common-helpers";
|
|
9
|
+
import { PrismaSchemaGenerator, TsSchemaGenerator } from "@zenstackhq/sdk";
|
|
10
|
+
import { createJiti } from "jiti";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
|
+
import terminalLink from "terminal-link";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import ora from "ora";
|
|
16
|
+
import { execSync } from "child_process";
|
|
17
|
+
import { fileURLToPath as fileURLToPath$1 } from "url";
|
|
18
|
+
import { DataFieldAttributeFactory, DataFieldFactory, DataModelFactory, EnumFactory } from "@zenstackhq/language/factory";
|
|
19
|
+
import { AstUtils } from "langium";
|
|
20
|
+
import { getLiteral, getLiteralArray, getStringLiteral } from "@zenstackhq/language/utils";
|
|
21
|
+
import { watch } from "chokidar";
|
|
22
|
+
import semver from "semver";
|
|
23
|
+
import { detect, resolveCommand } from "package-manager-detector";
|
|
24
|
+
import { execaCommand } from "execa";
|
|
25
|
+
import { ZenStackClient } from "@zenstackhq/orm";
|
|
26
|
+
import { MysqlDialect } from "@zenstackhq/orm/dialects/mysql";
|
|
27
|
+
import { PostgresDialect } from "@zenstackhq/orm/dialects/postgres";
|
|
28
|
+
import { SqliteDialect } from "@zenstackhq/orm/dialects/sqlite";
|
|
29
|
+
import { RPCApiHandler } from "@zenstackhq/server/api";
|
|
30
|
+
import { ZenStackMiddleware } from "@zenstackhq/server/express";
|
|
31
|
+
import cors from "cors";
|
|
32
|
+
import express from "express";
|
|
33
|
+
import { init } from "mixpanel";
|
|
34
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
35
|
+
import * as os$1 from "os";
|
|
36
|
+
import process$1, { env } from "node:process";
|
|
37
|
+
import os from "node:os";
|
|
38
|
+
//#region \0rolldown/runtime.js
|
|
39
|
+
var __defProp = Object.defineProperty;
|
|
40
|
+
var __exportAll = (all, no_symbols) => {
|
|
41
|
+
let target = {};
|
|
42
|
+
for (var name in all) __defProp(target, name, {
|
|
43
|
+
get: all[name],
|
|
44
|
+
enumerable: true
|
|
45
|
+
});
|
|
46
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
47
|
+
return target;
|
|
48
|
+
};
|
|
49
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/cli-error.ts
|
|
52
|
+
/**
|
|
53
|
+
* Indicating an error during CLI execution
|
|
54
|
+
*/
|
|
55
|
+
var CliError = class extends Error {};
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/actions/action-utils.ts
|
|
58
|
+
function getSchemaFile(file) {
|
|
59
|
+
if (file) {
|
|
60
|
+
if (!fs.existsSync(file)) throw new CliError(`Schema file not found: ${file}`);
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
63
|
+
const pkgJsonConfig = getPkgJsonConfig(process.cwd());
|
|
64
|
+
if (pkgJsonConfig.schema) {
|
|
65
|
+
if (!fs.existsSync(pkgJsonConfig.schema)) throw new CliError(`Schema file not found: ${pkgJsonConfig.schema}`);
|
|
66
|
+
if (fs.statSync(pkgJsonConfig.schema).isDirectory()) {
|
|
67
|
+
const schemaPath = path.join(pkgJsonConfig.schema, "schema.zmodel");
|
|
68
|
+
if (!fs.existsSync(schemaPath)) throw new CliError(`Schema file not found: ${schemaPath}`);
|
|
69
|
+
return schemaPath;
|
|
70
|
+
} else return pkgJsonConfig.schema;
|
|
71
|
+
}
|
|
72
|
+
if (fs.existsSync("./schema.zmodel")) return "./schema.zmodel";
|
|
73
|
+
else if (fs.existsSync("./zenstack/schema.zmodel")) return "./zenstack/schema.zmodel";
|
|
74
|
+
else throw new CliError("Schema file not found in default locations (\"./schema.zmodel\" or \"./zenstack/schema.zmodel\").");
|
|
75
|
+
}
|
|
76
|
+
async function loadSchemaDocument(schemaFile, opts = {}) {
|
|
77
|
+
const returnServices = opts.returnServices ?? false;
|
|
78
|
+
const loadResult = await loadDocument(schemaFile, [], opts.mergeImports ?? true);
|
|
79
|
+
if (!loadResult.success) {
|
|
80
|
+
loadResult.errors.forEach((err) => {
|
|
81
|
+
console.error(colors.red(err));
|
|
82
|
+
});
|
|
83
|
+
throw new CliError("Schema contains errors. See above for details.");
|
|
84
|
+
}
|
|
85
|
+
loadResult.warnings.forEach((warn) => {
|
|
86
|
+
console.warn(colors.yellow(warn));
|
|
87
|
+
});
|
|
88
|
+
if (returnServices) return {
|
|
89
|
+
model: loadResult.model,
|
|
90
|
+
services: loadResult.services
|
|
91
|
+
};
|
|
92
|
+
return loadResult.model;
|
|
93
|
+
}
|
|
94
|
+
function handleSubProcessError$1(err) {
|
|
95
|
+
if (err instanceof Error && "status" in err && typeof err.status === "number") process.exit(err.status);
|
|
96
|
+
else process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
async function generateTempPrismaSchema(zmodelPath, folder) {
|
|
99
|
+
const model = await loadSchemaDocument(zmodelPath);
|
|
100
|
+
if (!model.declarations.some(isDataSource)) throw new CliError("Schema must define a datasource");
|
|
101
|
+
const prismaSchema = await new PrismaSchemaGenerator(model).generate();
|
|
102
|
+
if (!folder) folder = path.dirname(zmodelPath);
|
|
103
|
+
const prismaSchemaFile = path.resolve(folder, "~schema.prisma");
|
|
104
|
+
fs.writeFileSync(prismaSchemaFile, prismaSchema);
|
|
105
|
+
return prismaSchemaFile;
|
|
106
|
+
}
|
|
107
|
+
function getPkgJsonConfig(startPath) {
|
|
108
|
+
const result = {
|
|
109
|
+
schema: void 0,
|
|
110
|
+
output: void 0,
|
|
111
|
+
seed: void 0
|
|
112
|
+
};
|
|
113
|
+
const pkgJsonFile = findUp(["package.json"], startPath, false);
|
|
114
|
+
if (!pkgJsonFile) return result;
|
|
115
|
+
let pkgJson = void 0;
|
|
116
|
+
try {
|
|
117
|
+
pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, "utf8"));
|
|
118
|
+
} catch {
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
if (pkgJson.zenstack && typeof pkgJson.zenstack === "object") {
|
|
122
|
+
result.schema = pkgJson.zenstack.schema && typeof pkgJson.zenstack.schema === "string" ? path.resolve(path.dirname(pkgJsonFile), pkgJson.zenstack.schema) : void 0;
|
|
123
|
+
result.output = pkgJson.zenstack.output && typeof pkgJson.zenstack.output === "string" ? path.resolve(path.dirname(pkgJsonFile), pkgJson.zenstack.output) : void 0;
|
|
124
|
+
result.seed = typeof pkgJson.zenstack.seed === "string" && pkgJson.zenstack.seed ? pkgJson.zenstack.seed : void 0;
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function findUp(names, cwd = process.cwd(), multiple = false, result = []) {
|
|
129
|
+
if (!names.some((name) => !!name)) return;
|
|
130
|
+
const target = names.find((name) => fs.existsSync(path.join(cwd, name)));
|
|
131
|
+
if (multiple === false && target) return path.resolve(cwd, target);
|
|
132
|
+
if (target) result.push(path.resolve(cwd, target));
|
|
133
|
+
const up = path.resolve(cwd, "..");
|
|
134
|
+
if (up === cwd) return multiple && result.length > 0 ? result : void 0;
|
|
135
|
+
return findUp(names, up, multiple, result);
|
|
136
|
+
}
|
|
137
|
+
async function requireDataSourceUrl(schemaFile) {
|
|
138
|
+
if (!(await loadSchemaDocument(schemaFile)).declarations.find(isDataSource)?.fields.some((f) => f.name === "url")) throw new CliError("The schema's \"datasource\" must have a \"url\" field to use this command.");
|
|
139
|
+
}
|
|
140
|
+
function getOutputPath(options, schemaFile) {
|
|
141
|
+
if (options.output) return options.output;
|
|
142
|
+
const pkgJsonConfig = getPkgJsonConfig(process.cwd());
|
|
143
|
+
if (pkgJsonConfig.output) return pkgJsonConfig.output;
|
|
144
|
+
else return path.dirname(schemaFile);
|
|
145
|
+
}
|
|
146
|
+
async function getZenStackPackages(searchPath) {
|
|
147
|
+
const pkgJsonFile = findUp(["package.json"], searchPath, false);
|
|
148
|
+
if (!pkgJsonFile) return [];
|
|
149
|
+
let pkgJson;
|
|
150
|
+
try {
|
|
151
|
+
pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, "utf8"));
|
|
152
|
+
} catch {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const packages = Array.from(new Set([...Object.keys(pkgJson.dependencies ?? {}), ...Object.keys(pkgJson.devDependencies ?? {})].filter((p) => p.startsWith("@zenstackhq/")))).sort();
|
|
156
|
+
const require = createRequire(pkgJsonFile);
|
|
157
|
+
return packages.map((pkg) => {
|
|
158
|
+
try {
|
|
159
|
+
const depPkgJson = require(`${pkg}/package.json`);
|
|
160
|
+
if (depPkgJson.private) return;
|
|
161
|
+
return {
|
|
162
|
+
pkg,
|
|
163
|
+
version: depPkgJson.version
|
|
164
|
+
};
|
|
165
|
+
} catch {
|
|
166
|
+
return {
|
|
167
|
+
pkg,
|
|
168
|
+
version: void 0
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}).filter((p) => !!p);
|
|
172
|
+
}
|
|
173
|
+
function getPluginProvider(plugin) {
|
|
174
|
+
const providerField = plugin.fields.find((f) => f.name === "provider");
|
|
175
|
+
invariant(providerField, `Plugin ${plugin.name} does not have a provider field`);
|
|
176
|
+
return providerField.value.value;
|
|
177
|
+
}
|
|
178
|
+
async function loadPluginModule(provider, basePath) {
|
|
179
|
+
if (provider.toLowerCase().endsWith(".zmodel")) return;
|
|
180
|
+
let moduleSpec = provider;
|
|
181
|
+
if (moduleSpec.startsWith(".")) moduleSpec = path.resolve(basePath, moduleSpec);
|
|
182
|
+
const importAsEsm = async (spec) => {
|
|
183
|
+
try {
|
|
184
|
+
const result = (await import(spec)).default;
|
|
185
|
+
return typeof result?.generate === "function" ? result : void 0;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
throw new CliError(`Failed to load plugin module from ${spec}: ${err.message}`);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const jiti = createJiti(pathToFileURL(basePath).toString());
|
|
191
|
+
const importAsTs = async (spec) => {
|
|
192
|
+
try {
|
|
193
|
+
const result = await jiti.import(spec, { default: true });
|
|
194
|
+
return typeof result?.generate === "function" ? result : void 0;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
throw new CliError(`Failed to load plugin module from ${spec}: ${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const esmSuffixes = [".js", ".mjs"];
|
|
200
|
+
const tsSuffixes = [".ts", ".mts"];
|
|
201
|
+
if (fs.existsSync(moduleSpec) && fs.statSync(moduleSpec).isFile()) {
|
|
202
|
+
if (esmSuffixes.some((suffix) => moduleSpec.endsWith(suffix))) return await importAsEsm(pathToFileURL(moduleSpec).toString());
|
|
203
|
+
if (tsSuffixes.some((suffix) => moduleSpec.endsWith(suffix))) return await importAsTs(moduleSpec);
|
|
204
|
+
}
|
|
205
|
+
for (const suffix of esmSuffixes) {
|
|
206
|
+
const indexPath = path.join(moduleSpec, `index${suffix}`);
|
|
207
|
+
if (fs.existsSync(indexPath)) return await importAsEsm(pathToFileURL(indexPath).toString());
|
|
208
|
+
}
|
|
209
|
+
for (const suffix of tsSuffixes) {
|
|
210
|
+
const indexPath = path.join(moduleSpec, `index${suffix}`);
|
|
211
|
+
if (fs.existsSync(indexPath)) return await importAsTs(indexPath);
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const result = await jiti.import(moduleSpec, { default: true });
|
|
215
|
+
return typeof result.generate === "function" ? result : void 0;
|
|
216
|
+
} catch {}
|
|
217
|
+
try {
|
|
218
|
+
const mod = await import(moduleSpec);
|
|
219
|
+
return typeof mod.default?.generate === "function" ? mod.default : void 0;
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const errorCode = err?.code;
|
|
222
|
+
if (errorCode === "ERR_MODULE_NOT_FOUND" || errorCode === "MODULE_NOT_FOUND") throw new CliError(`Cannot find plugin module "${provider}". Please make sure the package exists.`);
|
|
223
|
+
throw new CliError(`Failed to load plugin module "${provider}": ${err.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const FETCH_CLI_MAX_TIME = 1e3;
|
|
227
|
+
const CLI_CONFIG_ENDPOINT = "https://zenstack.dev/config/cli-v3.json";
|
|
228
|
+
const usageTipsSchema = z.object({ notifications: z.array(z.object({
|
|
229
|
+
title: z.string(),
|
|
230
|
+
url: z.url().optional(),
|
|
231
|
+
active: z.boolean()
|
|
232
|
+
})) });
|
|
233
|
+
/**
|
|
234
|
+
* Starts the usage tips fetch in the background. Returns a callback that, when invoked check if the fetch
|
|
235
|
+
* is complete. If not complete, it will wait until the max time is reached. After that, if fetch is still
|
|
236
|
+
* not complete, just return.
|
|
237
|
+
*/
|
|
238
|
+
function startUsageTipsFetch() {
|
|
239
|
+
let fetchedData = void 0;
|
|
240
|
+
let fetchComplete = false;
|
|
241
|
+
const start = Date.now();
|
|
242
|
+
const controller = new AbortController();
|
|
243
|
+
fetch(CLI_CONFIG_ENDPOINT, {
|
|
244
|
+
headers: { accept: "application/json" },
|
|
245
|
+
signal: controller.signal
|
|
246
|
+
}).then(async (res) => {
|
|
247
|
+
if (!res.ok) return;
|
|
248
|
+
const data = await res.json();
|
|
249
|
+
const parseResult = usageTipsSchema.safeParse(data);
|
|
250
|
+
if (parseResult.success) fetchedData = parseResult.data;
|
|
251
|
+
}).catch(() => {}).finally(() => {
|
|
252
|
+
fetchComplete = true;
|
|
253
|
+
});
|
|
254
|
+
return async () => {
|
|
255
|
+
const elapsed = Date.now() - start;
|
|
256
|
+
if (!fetchComplete && elapsed < FETCH_CLI_MAX_TIME) await new Promise((resolve) => setTimeout(resolve, FETCH_CLI_MAX_TIME - elapsed));
|
|
257
|
+
if (!fetchComplete) {
|
|
258
|
+
controller.abort();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!fetchedData) return;
|
|
262
|
+
const activeItems = fetchedData.notifications.filter((item) => item.active);
|
|
263
|
+
if (activeItems.length > 0) {
|
|
264
|
+
const item = activeItems[Math.floor(Math.random() * activeItems.length)];
|
|
265
|
+
if (item.url) console.log(terminalLink(item.title, item.url));
|
|
266
|
+
else console.log(item.title);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/actions/check.ts
|
|
272
|
+
/**
|
|
273
|
+
* CLI action for checking a schema's validity.
|
|
274
|
+
*/
|
|
275
|
+
async function run$8(options) {
|
|
276
|
+
const schemaFile = getSchemaFile(options.schema);
|
|
277
|
+
try {
|
|
278
|
+
await checkPluginResolution(schemaFile, await loadSchemaDocument(schemaFile));
|
|
279
|
+
console.log(colors.green("✓ Schema validation completed successfully."));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(colors.red("✗ Schema validation failed."));
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function checkPluginResolution(schemaFile, model) {
|
|
286
|
+
const plugins = model.declarations.filter(isPlugin);
|
|
287
|
+
for (const plugin of plugins) {
|
|
288
|
+
const provider = getPluginProvider(plugin);
|
|
289
|
+
if (!provider.startsWith("@core/")) {
|
|
290
|
+
const pluginSourcePath = plugin.$cstNode?.parent?.element.$document?.uri?.fsPath ?? schemaFile;
|
|
291
|
+
await loadPluginModule(provider, path.dirname(pluginSourcePath));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/utils/exec-utils.ts
|
|
297
|
+
/**
|
|
298
|
+
* Utility for executing command synchronously and prints outputs on current console
|
|
299
|
+
*/
|
|
300
|
+
function execSync$1(cmd, options) {
|
|
301
|
+
const { env, ...restOptions } = options ?? {};
|
|
302
|
+
const mergedEnv = env ? {
|
|
303
|
+
...process.env,
|
|
304
|
+
...env
|
|
305
|
+
} : void 0;
|
|
306
|
+
execSync(cmd, {
|
|
307
|
+
encoding: "utf-8",
|
|
308
|
+
stdio: options?.stdio ?? "inherit",
|
|
309
|
+
env: mergedEnv,
|
|
310
|
+
...restOptions
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Utility for running package commands through npx/bunx
|
|
315
|
+
*/
|
|
316
|
+
function execPackage(cmd, options) {
|
|
317
|
+
execSync$1(`${process?.versions?.["bun"] ? "bunx" : "npx"} ${cmd}`, options);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Utility for running prisma commands
|
|
321
|
+
*/
|
|
322
|
+
function execPrisma(args, options) {
|
|
323
|
+
let prismaPath;
|
|
324
|
+
try {
|
|
325
|
+
if (typeof import.meta.resolve === "function") prismaPath = fileURLToPath$1(import.meta.resolve("prisma/build/index.js"));
|
|
326
|
+
else prismaPath = __require.resolve("prisma/build/index.js");
|
|
327
|
+
} catch {}
|
|
328
|
+
const _options = {
|
|
329
|
+
...options,
|
|
330
|
+
env: {
|
|
331
|
+
...options?.env,
|
|
332
|
+
PRISMA_HIDE_UPDATE_MESSAGE: "1"
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
if (!prismaPath) {
|
|
336
|
+
execPackage(`prisma ${args}`, _options);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
execSync$1(`node "${prismaPath}" ${args}`, _options);
|
|
340
|
+
}
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/actions/pull/utils.ts
|
|
343
|
+
function isDatabaseManagedAttribute(name) {
|
|
344
|
+
return [
|
|
345
|
+
"@relation",
|
|
346
|
+
"@id",
|
|
347
|
+
"@unique"
|
|
348
|
+
].includes(name) || name.startsWith("@db.");
|
|
349
|
+
}
|
|
350
|
+
function getDatasource(model) {
|
|
351
|
+
const datasource = model.declarations.find((d) => d.$type === "DataSource");
|
|
352
|
+
if (!datasource) throw new CliError("No datasource declaration found in the schema.");
|
|
353
|
+
const urlField = datasource.fields.find((f) => f.name === "url");
|
|
354
|
+
if (!urlField) throw new CliError(`No url field found in the datasource declaration.`);
|
|
355
|
+
let url = getStringLiteral(urlField.value);
|
|
356
|
+
if (!url && isInvocationExpr(urlField.value)) {
|
|
357
|
+
const envName = getStringLiteral(urlField.value.args[0]?.value);
|
|
358
|
+
if (!envName) throw new CliError("The url field must be a string literal or an env().");
|
|
359
|
+
if (!process.env[envName]) throw new CliError(`Environment variable ${envName} is not set, please set it to the database connection string.`);
|
|
360
|
+
url = process.env[envName];
|
|
361
|
+
}
|
|
362
|
+
if (!url) throw new CliError("The url field must be a string literal or an env().");
|
|
363
|
+
if (url.startsWith("file:")) {
|
|
364
|
+
url = new URL(url, `file:${model.$document.uri.path}`).pathname;
|
|
365
|
+
if (process.platform === "win32" && url[0] === "/") url = url.slice(1);
|
|
366
|
+
}
|
|
367
|
+
const defaultSchemaField = datasource.fields.find((f) => f.name === "defaultSchema");
|
|
368
|
+
const defaultSchema = defaultSchemaField && getStringLiteral(defaultSchemaField.value) || "public";
|
|
369
|
+
const schemasField = datasource.fields.find((f) => f.name === "schemas");
|
|
370
|
+
const schemas = schemasField && getLiteralArray(schemasField.value)?.filter((s) => s !== void 0) || [];
|
|
371
|
+
const provider = getStringLiteral(datasource.fields.find((f) => f.name === "provider")?.value);
|
|
372
|
+
if (!provider) throw new CliError(`Datasource "${datasource.name}" is missing a "provider" field.`);
|
|
373
|
+
return {
|
|
374
|
+
name: datasource.name,
|
|
375
|
+
provider,
|
|
376
|
+
url,
|
|
377
|
+
defaultSchema,
|
|
378
|
+
schemas,
|
|
379
|
+
allSchemas: [defaultSchema, ...schemas]
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function getDbName(decl, includeSchema = false) {
|
|
383
|
+
if (!("attributes" in decl)) return decl.name;
|
|
384
|
+
const schemaAttr = decl.attributes.find((a) => a.decl.ref?.name === "@@schema");
|
|
385
|
+
let schema = "public";
|
|
386
|
+
if (schemaAttr) {
|
|
387
|
+
const schemaAttrValue = schemaAttr.args[0]?.value;
|
|
388
|
+
if (schemaAttrValue?.$type === "StringLiteral") schema = schemaAttrValue.value;
|
|
389
|
+
}
|
|
390
|
+
const formatName = (name) => `${schema && includeSchema ? `${schema}.` : ""}${name}`;
|
|
391
|
+
const nameAttr = decl.attributes.find((a) => a.decl.ref?.name === "@@map" || a.decl.ref?.name === "@map");
|
|
392
|
+
if (!nameAttr) return formatName(decl.name);
|
|
393
|
+
const attrValue = nameAttr.args[0]?.value;
|
|
394
|
+
if (attrValue?.$type !== "StringLiteral") return formatName(decl.name);
|
|
395
|
+
return formatName(attrValue.value);
|
|
396
|
+
}
|
|
397
|
+
function getRelationFkName(decl) {
|
|
398
|
+
return ((decl?.attributes.find((a) => a.decl.ref?.name === "@relation"))?.args.find((a) => a.name === "map")?.value)?.value;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Gets the FK field names from the @relation attribute's `fields` argument.
|
|
402
|
+
* Returns a sorted, comma-separated string of field names for comparison.
|
|
403
|
+
* e.g., @relation(fields: [userId], references: [id]) -> "userId"
|
|
404
|
+
* e.g., @relation(fields: [postId, tagId], references: [id, id]) -> "postId,tagId"
|
|
405
|
+
*/
|
|
406
|
+
function getRelationFieldsKey(decl) {
|
|
407
|
+
const relationAttr = decl?.attributes.find((a) => a.decl.ref?.name === "@relation");
|
|
408
|
+
if (!relationAttr) return void 0;
|
|
409
|
+
const fieldsArg = relationAttr.args.find((a) => a.name === "fields")?.value;
|
|
410
|
+
if (!fieldsArg || fieldsArg.$type !== "ArrayExpr") return void 0;
|
|
411
|
+
const fieldNames = fieldsArg.items.filter((item) => item.$type === "ReferenceExpr").map((item) => item.target?.$refText || item.target?.ref?.name).filter((name) => !!name).sort();
|
|
412
|
+
return fieldNames.length > 0 ? fieldNames.join(",") : void 0;
|
|
413
|
+
}
|
|
414
|
+
function getDeclarationRef(type, name, services) {
|
|
415
|
+
const node = services.shared.workspace.IndexManager.allElements(type).find((m) => m.node && getDbName(m.node) === name)?.node;
|
|
416
|
+
if (!node) throw new CliError(`Declaration not found: ${name}`);
|
|
417
|
+
return node;
|
|
418
|
+
}
|
|
419
|
+
function getEnumRef(name, services) {
|
|
420
|
+
return getDeclarationRef("Enum", name, services);
|
|
421
|
+
}
|
|
422
|
+
function getAttributeRef(name, services) {
|
|
423
|
+
return getDeclarationRef("Attribute", name, services);
|
|
424
|
+
}
|
|
425
|
+
function getFunctionRef(name, services) {
|
|
426
|
+
return getDeclarationRef("FunctionDecl", name, services);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Normalize a default value string for a Float field.
|
|
430
|
+
* - Integer strings get `.0` appended
|
|
431
|
+
* - Decimal strings are preserved as-is
|
|
432
|
+
*/
|
|
433
|
+
function normalizeFloatDefault(val) {
|
|
434
|
+
if (/^-?\d+$/.test(val)) return (ab) => ab.NumberLiteral.setValue(val + ".0");
|
|
435
|
+
if (/^-?\d+\.\d+$/.test(val)) return (ab) => ab.NumberLiteral.setValue(val);
|
|
436
|
+
return (ab) => ab.NumberLiteral.setValue(val);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Normalize a default value string for a Decimal field.
|
|
440
|
+
* - Integer strings get `.00` appended
|
|
441
|
+
* - Decimal strings are normalized to minimum 2 decimal places, stripping excess trailing zeros
|
|
442
|
+
*/
|
|
443
|
+
function normalizeDecimalDefault(val) {
|
|
444
|
+
if (/^-?\d+$/.test(val)) return (ab) => ab.NumberLiteral.setValue(val + ".00");
|
|
445
|
+
if (/^-?\d+\.\d+$/.test(val)) {
|
|
446
|
+
const [integerPart, fractionalPart] = val.split(".");
|
|
447
|
+
let normalized = fractionalPart.replace(/0+$/, "");
|
|
448
|
+
if (normalized.length < 2) normalized = normalized.padEnd(2, "0");
|
|
449
|
+
return (ab) => ab.NumberLiteral.setValue(`${integerPart}.${normalized}`);
|
|
450
|
+
}
|
|
451
|
+
return (ab) => ab.NumberLiteral.setValue(val);
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/actions/pull/casing.ts
|
|
455
|
+
function resolveNameCasing(casing, originalName) {
|
|
456
|
+
let name = originalName;
|
|
457
|
+
const fieldPrefix = /[0-9]/g.test(name.charAt(0)) ? "_" : "";
|
|
458
|
+
switch (casing) {
|
|
459
|
+
case "pascal":
|
|
460
|
+
name = toPascalCase(originalName);
|
|
461
|
+
break;
|
|
462
|
+
case "camel":
|
|
463
|
+
name = toCamelCase(originalName);
|
|
464
|
+
break;
|
|
465
|
+
case "snake":
|
|
466
|
+
name = toSnakeCase(originalName);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
modified: name !== originalName || fieldPrefix !== "",
|
|
471
|
+
name: `${fieldPrefix}${name}`
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function isAllUpperCase(str) {
|
|
475
|
+
return str === str.toUpperCase();
|
|
476
|
+
}
|
|
477
|
+
function toPascalCase(str) {
|
|
478
|
+
if (isAllUpperCase(str)) return str;
|
|
479
|
+
return str.replace(/[_\- ]+(\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
|
|
480
|
+
}
|
|
481
|
+
function toCamelCase(str) {
|
|
482
|
+
if (isAllUpperCase(str)) return str;
|
|
483
|
+
return str.replace(/[_\- ]+(\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toLowerCase());
|
|
484
|
+
}
|
|
485
|
+
function toSnakeCase(str) {
|
|
486
|
+
if (isAllUpperCase(str)) return str;
|
|
487
|
+
return str.replace(/[- ]+/g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
488
|
+
}
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/actions/pull/index.ts
|
|
491
|
+
function syncEnums({ dbEnums, model, oldModel, provider, options, services, defaultSchema }) {
|
|
492
|
+
if (provider.isSupportedFeature("NativeEnum")) for (const dbEnum of dbEnums) {
|
|
493
|
+
const { modified, name } = resolveNameCasing(options.modelCasing, dbEnum.enum_type);
|
|
494
|
+
if (modified) console.log(colors.gray(`Mapping enum ${dbEnum.enum_type} to ${name}`));
|
|
495
|
+
const factory = new EnumFactory().setName(name);
|
|
496
|
+
if (modified || options.alwaysMap) factory.addAttribute((builder) => builder.setDecl(getAttributeRef("@@map", services)).addArg((argBuilder) => argBuilder.StringLiteral.setValue(dbEnum.enum_type)));
|
|
497
|
+
dbEnum.values.forEach((v) => {
|
|
498
|
+
const { name, modified } = resolveNameCasing(options.fieldCasing, v);
|
|
499
|
+
factory.addField((builder) => {
|
|
500
|
+
builder.setName(name);
|
|
501
|
+
if (modified || options.alwaysMap) builder.addAttribute((builder) => builder.setDecl(getAttributeRef("@map", services)).addArg((argBuilder) => argBuilder.StringLiteral.setValue(v)));
|
|
502
|
+
return builder;
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
if (dbEnum.schema_name && dbEnum.schema_name !== "" && dbEnum.schema_name !== defaultSchema) factory.addAttribute((b) => b.setDecl(getAttributeRef("@@schema", services)).addArg((a) => a.StringLiteral.setValue(dbEnum.schema_name)));
|
|
506
|
+
model.declarations.push(factory.get({ $container: model }));
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
const dummyBuildReference = (_node, _property, _refNode, refText) => ({ $refText: refText });
|
|
510
|
+
oldModel.declarations.filter((d) => isEnum(d)).forEach((d) => {
|
|
511
|
+
const copy = AstUtils.copyAstNode(d, dummyBuildReference);
|
|
512
|
+
copy.$container = model;
|
|
513
|
+
model.declarations.push(copy);
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function syncTable({ model, provider, table, services, options, defaultSchema }) {
|
|
518
|
+
const idAttribute = getAttributeRef("@id", services);
|
|
519
|
+
const modelIdAttribute = getAttributeRef("@@id", services);
|
|
520
|
+
const uniqueAttribute = getAttributeRef("@unique", services);
|
|
521
|
+
const modelUniqueAttribute = getAttributeRef("@@unique", services);
|
|
522
|
+
const fieldMapAttribute = getAttributeRef("@map", services);
|
|
523
|
+
const tableMapAttribute = getAttributeRef("@@map", services);
|
|
524
|
+
const modelindexAttribute = getAttributeRef("@@index", services);
|
|
525
|
+
const relations = [];
|
|
526
|
+
const { name, modified } = resolveNameCasing(options.modelCasing, table.name);
|
|
527
|
+
const multiPk = table.columns.filter((c) => c.pk).length > 1;
|
|
528
|
+
const modelFactory = new DataModelFactory().setName(name).setIsView(table.type === "view");
|
|
529
|
+
modelFactory.setContainer(model);
|
|
530
|
+
if (modified || options.alwaysMap) modelFactory.addAttribute((builder) => builder.setDecl(tableMapAttribute).addArg((argBuilder) => argBuilder.StringLiteral.setValue(table.name)));
|
|
531
|
+
const fkGroups = /* @__PURE__ */ new Map();
|
|
532
|
+
table.columns.forEach((column) => {
|
|
533
|
+
if (column.foreign_key_table && column.foreign_key_name) {
|
|
534
|
+
const group = fkGroups.get(column.foreign_key_name) ?? [];
|
|
535
|
+
group.push(column);
|
|
536
|
+
fkGroups.set(column.foreign_key_name, group);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
for (const [fkName, fkColumns] of fkGroups) {
|
|
540
|
+
const firstCol = fkColumns[0];
|
|
541
|
+
const isSingleColumnPk = fkColumns.length === 1 && !multiPk && firstCol.pk;
|
|
542
|
+
const isUniqueRelation = fkColumns.length === 1 && firstCol.unique || isSingleColumnPk;
|
|
543
|
+
relations.push({
|
|
544
|
+
schema: table.schema,
|
|
545
|
+
table: table.name,
|
|
546
|
+
columns: fkColumns.map((c) => c.name),
|
|
547
|
+
type: "one",
|
|
548
|
+
fk_name: fkName,
|
|
549
|
+
foreign_key_on_delete: firstCol.foreign_key_on_delete,
|
|
550
|
+
foreign_key_on_update: firstCol.foreign_key_on_update,
|
|
551
|
+
nullable: firstCol.nullable,
|
|
552
|
+
references: {
|
|
553
|
+
schema: firstCol.foreign_key_schema,
|
|
554
|
+
table: firstCol.foreign_key_table,
|
|
555
|
+
columns: fkColumns.map((c) => c.foreign_key_column),
|
|
556
|
+
type: isUniqueRelation ? "one" : "many"
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
table.columns.forEach((column) => {
|
|
561
|
+
const { name, modified } = resolveNameCasing(options.fieldCasing, column.name);
|
|
562
|
+
const builtinType = provider.getBuiltinType(column.datatype);
|
|
563
|
+
modelFactory.addField((builder) => {
|
|
564
|
+
builder.setName(name);
|
|
565
|
+
builder.setType((typeBuilder) => {
|
|
566
|
+
typeBuilder.setArray(builtinType.isArray);
|
|
567
|
+
typeBuilder.setOptional(builtinType.isArray ? false : column.nullable);
|
|
568
|
+
if (column.computed) typeBuilder.setUnsupported((unsupportedBuilder) => unsupportedBuilder.setValue((lt) => lt.StringLiteral.setValue(column.datatype)));
|
|
569
|
+
else if (column.datatype === "enum") {
|
|
570
|
+
const ref = model.declarations.find((d) => isEnum(d) && getDbName(d) === column.datatype_name);
|
|
571
|
+
if (!ref) throw new CliError(`Enum ${column.datatype_name} not found`);
|
|
572
|
+
typeBuilder.setReference(ref);
|
|
573
|
+
} else if (builtinType.type !== "Unsupported") typeBuilder.setType(builtinType.type);
|
|
574
|
+
else typeBuilder.setUnsupported((unsupportedBuilder) => unsupportedBuilder.setValue((lt) => lt.StringLiteral.setValue(column.datatype)));
|
|
575
|
+
return typeBuilder;
|
|
576
|
+
});
|
|
577
|
+
if (column.pk && !multiPk) builder.addAttribute((b) => b.setDecl(idAttribute));
|
|
578
|
+
provider.getFieldAttributes({
|
|
579
|
+
fieldName: column.name,
|
|
580
|
+
fieldType: builtinType.type,
|
|
581
|
+
datatype: column.datatype,
|
|
582
|
+
length: column.length,
|
|
583
|
+
precision: column.precision,
|
|
584
|
+
services
|
|
585
|
+
}).forEach(builder.addAttribute.bind(builder));
|
|
586
|
+
if (column.default && !column.computed) {
|
|
587
|
+
const defaultExprBuilder = provider.getDefaultValue({
|
|
588
|
+
fieldType: builtinType.type,
|
|
589
|
+
datatype: column.datatype,
|
|
590
|
+
datatype_name: column.datatype_name,
|
|
591
|
+
defaultValue: column.default,
|
|
592
|
+
services,
|
|
593
|
+
enums: model.declarations.filter((d) => d.$type === "Enum")
|
|
594
|
+
});
|
|
595
|
+
if (defaultExprBuilder) {
|
|
596
|
+
const defaultAttr = new DataFieldAttributeFactory().setDecl(getAttributeRef("@default", services)).addArg(defaultExprBuilder);
|
|
597
|
+
builder.addAttribute(defaultAttr);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (column.unique && !column.pk) builder.addAttribute((b) => {
|
|
601
|
+
b.setDecl(uniqueAttribute);
|
|
602
|
+
if (!(!column.unique_name || column.unique_name === `${table.name}_${column.name}_key` || column.unique_name === column.name)) b.addArg((ab) => ab.StringLiteral.setValue(column.unique_name), "map");
|
|
603
|
+
return b;
|
|
604
|
+
});
|
|
605
|
+
if (modified || options.alwaysMap) builder.addAttribute((ab) => ab.setDecl(fieldMapAttribute).addArg((ab) => ab.StringLiteral.setValue(column.name)));
|
|
606
|
+
return builder;
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
const pkColumns = table.columns.filter((c) => c.pk).map((c) => c.name);
|
|
610
|
+
if (multiPk) modelFactory.addAttribute((builder) => builder.setDecl(modelIdAttribute).addArg((argBuilder) => {
|
|
611
|
+
const arrayExpr = argBuilder.ArrayExpr;
|
|
612
|
+
pkColumns.forEach((c) => {
|
|
613
|
+
const ref = modelFactory.node.fields.find((f) => getDbName(f) === c);
|
|
614
|
+
if (!ref) throw new CliError(`Field ${c} not found`);
|
|
615
|
+
arrayExpr.addItem((itemBuilder) => itemBuilder.ReferenceExpr.setTarget(ref));
|
|
616
|
+
});
|
|
617
|
+
return arrayExpr;
|
|
618
|
+
}));
|
|
619
|
+
if (!(table.columns.some((c) => c.unique || c.pk) || table.indexes.some((i) => i.unique))) {
|
|
620
|
+
modelFactory.addAttribute((a) => a.setDecl(getAttributeRef("@@ignore", services)));
|
|
621
|
+
modelFactory.addComment("/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Zenstack Client.");
|
|
622
|
+
}
|
|
623
|
+
table.indexes.reverse().sort((a, b) => {
|
|
624
|
+
if (a.unique && !b.unique) return -1;
|
|
625
|
+
if (!a.unique && b.unique) return 1;
|
|
626
|
+
return 0;
|
|
627
|
+
}).forEach((index) => {
|
|
628
|
+
if (index.predicate) {
|
|
629
|
+
console.warn(colors.yellow(`These constraints are not supported by Zenstack. Read more: https://pris.ly/d/check-constraints\n- Model: "${table.name}", constraint: "${index.name}"`));
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (index.columns.find((c) => c.expression)) {
|
|
633
|
+
console.warn(colors.yellow(`These constraints are not supported by Zenstack. Read more: https://pris.ly/d/check-constraints\n- Model: "${table.name}", constraint: "${index.name}"`));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (index.primary) return;
|
|
637
|
+
if (index.columns.length === 1 && (index.columns.find((c) => pkColumns.includes(c.name)) || index.unique)) return;
|
|
638
|
+
modelFactory.addAttribute((builder) => {
|
|
639
|
+
const attr = builder.setDecl(index.unique ? modelUniqueAttribute : modelindexAttribute).addArg((argBuilder) => {
|
|
640
|
+
const arrayExpr = argBuilder.ArrayExpr;
|
|
641
|
+
index.columns.forEach((c) => {
|
|
642
|
+
const ref = modelFactory.node.fields.find((f) => getDbName(f) === c.name);
|
|
643
|
+
if (!ref) throw new CliError(`Column ${c.name} not found in model ${table.name}`);
|
|
644
|
+
arrayExpr.addItem((itemBuilder) => {
|
|
645
|
+
const refExpr = itemBuilder.ReferenceExpr.setTarget(ref);
|
|
646
|
+
if (c.order && c.order !== "ASC") refExpr.addArg((ab) => ab.StringLiteral.setValue("DESC"), "sort");
|
|
647
|
+
return refExpr;
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
return arrayExpr;
|
|
651
|
+
});
|
|
652
|
+
const suffix = index.unique ? "_key" : "_idx";
|
|
653
|
+
if (index.name !== `${table.name}_${index.columns.map((c) => c.name).join("_")}${suffix}`) attr.addArg((argBuilder) => argBuilder.StringLiteral.setValue(index.name), "map");
|
|
654
|
+
return attr;
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
if (table.schema && table.schema !== "" && table.schema !== defaultSchema) modelFactory.addAttribute((b) => b.setDecl(getAttributeRef("@@schema", services)).addArg((a) => a.StringLiteral.setValue(table.schema)));
|
|
658
|
+
model.declarations.push(modelFactory.node);
|
|
659
|
+
return relations;
|
|
660
|
+
}
|
|
661
|
+
function syncRelation({ model, relation, services, options, selfRelation, similarRelations }) {
|
|
662
|
+
const idAttribute = getAttributeRef("@id", services);
|
|
663
|
+
const uniqueAttribute = getAttributeRef("@unique", services);
|
|
664
|
+
const relationAttribute = getAttributeRef("@relation", services);
|
|
665
|
+
const fieldMapAttribute = getAttributeRef("@map", services);
|
|
666
|
+
const tableMapAttribute = getAttributeRef("@@map", services);
|
|
667
|
+
const includeRelationName = selfRelation || similarRelations > 0;
|
|
668
|
+
if (!idAttribute || !uniqueAttribute || !relationAttribute || !fieldMapAttribute || !tableMapAttribute) throw new CliError("Cannot find required attributes in the model.");
|
|
669
|
+
const sourceModel = model.declarations.find((d) => d.$type === "DataModel" && getDbName(d) === relation.table);
|
|
670
|
+
if (!sourceModel) return;
|
|
671
|
+
const sourceFields = [];
|
|
672
|
+
for (const colName of relation.columns) {
|
|
673
|
+
const idx = sourceModel.fields.findIndex((f) => getDbName(f) === colName);
|
|
674
|
+
const field = sourceModel.fields[idx];
|
|
675
|
+
if (!field) return;
|
|
676
|
+
sourceFields.push({
|
|
677
|
+
field,
|
|
678
|
+
index: idx
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
const targetModel = model.declarations.find((d) => d.$type === "DataModel" && getDbName(d) === relation.references.table);
|
|
682
|
+
if (!targetModel) return;
|
|
683
|
+
const targetFields = [];
|
|
684
|
+
for (const colName of relation.references.columns) {
|
|
685
|
+
const field = targetModel.fields.find((f) => getDbName(f) === colName);
|
|
686
|
+
if (!field) return;
|
|
687
|
+
targetFields.push(field);
|
|
688
|
+
}
|
|
689
|
+
const firstSourceField = sourceFields[0].field;
|
|
690
|
+
const firstSourceFieldId = sourceFields[0].index;
|
|
691
|
+
const firstColumn = relation.columns[0];
|
|
692
|
+
const fieldPrefix = /[0-9]/g.test(sourceModel.name.charAt(0)) ? "_" : "";
|
|
693
|
+
const relationName = `${relation.table}${similarRelations > 0 ? `_${firstColumn}` : ""}To${relation.references.table}`;
|
|
694
|
+
const sourceNameFromReference = firstSourceField.name.toLowerCase().endsWith("id") ? `${resolveNameCasing(options.fieldCasing, firstSourceField.name.slice(0, -2)).name}${relation.type === "many" ? "s" : ""}` : void 0;
|
|
695
|
+
const sourceFieldFromReference = sourceModel.fields.find((f) => f.name === sourceNameFromReference);
|
|
696
|
+
let { name: sourceFieldName } = resolveNameCasing(options.fieldCasing, similarRelations > 0 ? `${fieldPrefix}${lowerCaseFirst(sourceModel.name)}_${firstColumn}` : `${(!sourceFieldFromReference ? sourceNameFromReference : void 0) || lowerCaseFirst(resolveNameCasing(options.fieldCasing, targetModel.name).name)}${relation.type === "many" ? "s" : ""}`);
|
|
697
|
+
if (sourceModel.fields.find((f) => f.name === sourceFieldName)) sourceFieldName = `${sourceFieldName}To${lowerCaseFirst(targetModel.name)}_${relation.references.columns[0]}`;
|
|
698
|
+
const sourceFieldFactory = new DataFieldFactory().setContainer(sourceModel).setName(sourceFieldName).setType((tb) => tb.setOptional(relation.nullable).setArray(relation.type === "many").setReference(targetModel));
|
|
699
|
+
sourceFieldFactory.addAttribute((ab) => {
|
|
700
|
+
ab.setDecl(relationAttribute);
|
|
701
|
+
if (includeRelationName) ab.addArg((ab) => ab.StringLiteral.setValue(relationName));
|
|
702
|
+
ab.addArg((ab) => {
|
|
703
|
+
const arrayExpr = ab.ArrayExpr;
|
|
704
|
+
for (const { field } of sourceFields) arrayExpr.addItem((aeb) => aeb.ReferenceExpr.setTarget(field));
|
|
705
|
+
return arrayExpr;
|
|
706
|
+
}, "fields");
|
|
707
|
+
ab.addArg((ab) => {
|
|
708
|
+
const arrayExpr = ab.ArrayExpr;
|
|
709
|
+
for (const field of targetFields) arrayExpr.addItem((aeb) => aeb.ReferenceExpr.setTarget(field));
|
|
710
|
+
return arrayExpr;
|
|
711
|
+
}, "references");
|
|
712
|
+
const onDeleteDefault = relation.nullable ? "SET NULL" : "RESTRICT";
|
|
713
|
+
if (relation.foreign_key_on_delete && relation.foreign_key_on_delete !== onDeleteDefault) {
|
|
714
|
+
const enumRef = getEnumRef("ReferentialAction", services);
|
|
715
|
+
if (!enumRef) throw new CliError("ReferentialAction enum not found");
|
|
716
|
+
const enumFieldRef = enumRef.fields.find((f) => f.name.toLowerCase() === relation.foreign_key_on_delete.replace(/ /g, "").toLowerCase());
|
|
717
|
+
if (!enumFieldRef) throw new CliError(`ReferentialAction ${relation.foreign_key_on_delete} not found`);
|
|
718
|
+
ab.addArg((a) => a.ReferenceExpr.setTarget(enumFieldRef), "onDelete");
|
|
719
|
+
}
|
|
720
|
+
if (relation.foreign_key_on_update && relation.foreign_key_on_update !== "CASCADE") {
|
|
721
|
+
const enumRef = getEnumRef("ReferentialAction", services);
|
|
722
|
+
if (!enumRef) throw new CliError("ReferentialAction enum not found");
|
|
723
|
+
const enumFieldRef = enumRef.fields.find((f) => f.name.toLowerCase() === relation.foreign_key_on_update.replace(/ /g, "").toLowerCase());
|
|
724
|
+
if (!enumFieldRef) throw new CliError(`ReferentialAction ${relation.foreign_key_on_update} not found`);
|
|
725
|
+
ab.addArg((a) => a.ReferenceExpr.setTarget(enumFieldRef), "onUpdate");
|
|
726
|
+
}
|
|
727
|
+
const defaultFkName = `${relation.table}_${relation.columns.join("_")}_fkey`;
|
|
728
|
+
if (relation.fk_name && relation.fk_name !== defaultFkName) ab.addArg((ab) => ab.StringLiteral.setValue(relation.fk_name), "map");
|
|
729
|
+
return ab;
|
|
730
|
+
});
|
|
731
|
+
sourceModel.fields.splice(firstSourceFieldId, 0, sourceFieldFactory.node);
|
|
732
|
+
const oppositeFieldPrefix = /[0-9]/g.test(targetModel.name.charAt(0)) ? "_" : "";
|
|
733
|
+
let { name: oppositeFieldName } = resolveNameCasing(options.fieldCasing, similarRelations > 0 ? `${oppositeFieldPrefix}${lowerCaseFirst(sourceModel.name)}_${firstColumn}` : `${lowerCaseFirst(resolveNameCasing(options.fieldCasing, sourceModel.name).name)}${relation.references.type === "many" ? "s" : ""}`);
|
|
734
|
+
if (targetModel.fields.find((f) => f.name === oppositeFieldName)) ({name: oppositeFieldName} = resolveNameCasing(options.fieldCasing, `${lowerCaseFirst(sourceModel.name)}_${firstColumn}To${relation.references.table}_${relation.references.columns[0]}`));
|
|
735
|
+
const targetFieldFactory = new DataFieldFactory().setContainer(targetModel).setName(oppositeFieldName).setType((tb) => tb.setOptional(relation.references.type === "one").setArray(relation.references.type === "many").setReference(sourceModel));
|
|
736
|
+
if (includeRelationName) targetFieldFactory.addAttribute((ab) => ab.setDecl(relationAttribute).addArg((ab) => ab.StringLiteral.setValue(relationName)));
|
|
737
|
+
targetModel.fields.push(targetFieldFactory.node);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Consolidates per-column enums back to shared enums when possible.
|
|
741
|
+
*
|
|
742
|
+
* MySQL doesn't have named enum types — each column gets a synthetic enum
|
|
743
|
+
* (e.g., `UserStatus`, `GroupStatus`). When the original schema used a shared
|
|
744
|
+
* enum (e.g., `Status`) across multiple fields, this function detects the
|
|
745
|
+
* mapping via field references and consolidates the synthetic enums back into
|
|
746
|
+
* the original shared enum so the merge phase can match them correctly.
|
|
747
|
+
*/
|
|
748
|
+
function consolidateEnums({ newModel, oldModel }) {
|
|
749
|
+
const newEnums = newModel.declarations.filter((d) => isEnum(d));
|
|
750
|
+
const newDataModels = newModel.declarations.filter((d) => d.$type === "DataModel");
|
|
751
|
+
const oldDataModels = oldModel.declarations.filter((d) => d.$type === "DataModel");
|
|
752
|
+
const enumMapping = /* @__PURE__ */ new Map();
|
|
753
|
+
for (const newEnum of newEnums) for (const newDM of newDataModels) {
|
|
754
|
+
for (const field of newDM.fields) {
|
|
755
|
+
if (field.$type !== "DataField" || field.type.reference?.ref !== newEnum) continue;
|
|
756
|
+
const oldDM = oldDataModels.find((d) => getDbName(d) === getDbName(newDM));
|
|
757
|
+
if (!oldDM) continue;
|
|
758
|
+
const oldField = oldDM.fields.find((f) => getDbName(f) === getDbName(field));
|
|
759
|
+
if (!oldField || oldField.$type !== "DataField" || !oldField.type.reference?.ref) continue;
|
|
760
|
+
const oldEnum = oldField.type.reference.ref;
|
|
761
|
+
if (!isEnum(oldEnum)) continue;
|
|
762
|
+
enumMapping.set(newEnum, oldEnum);
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
if (enumMapping.has(newEnum)) break;
|
|
766
|
+
}
|
|
767
|
+
const reverseMapping = /* @__PURE__ */ new Map();
|
|
768
|
+
for (const [newEnum, oldEnum] of enumMapping) {
|
|
769
|
+
if (!reverseMapping.has(oldEnum)) reverseMapping.set(oldEnum, []);
|
|
770
|
+
reverseMapping.get(oldEnum).push(newEnum);
|
|
771
|
+
}
|
|
772
|
+
for (const [oldEnum, newEnumsGroup] of reverseMapping) {
|
|
773
|
+
const keepEnum = newEnumsGroup[0];
|
|
774
|
+
if (newEnumsGroup.length === 1 && keepEnum.name === oldEnum.name) continue;
|
|
775
|
+
const oldValues = new Set(oldEnum.fields.map((f) => getDbName(f)));
|
|
776
|
+
if (!newEnumsGroup.every((ne) => {
|
|
777
|
+
const newValues = new Set(ne.fields.map((f) => getDbName(f)));
|
|
778
|
+
return oldValues.size === newValues.size && [...oldValues].every((v) => newValues.has(v));
|
|
779
|
+
})) continue;
|
|
780
|
+
keepEnum.name = oldEnum.name;
|
|
781
|
+
keepEnum.attributes = oldEnum.attributes.map((attr) => {
|
|
782
|
+
return {
|
|
783
|
+
...attr,
|
|
784
|
+
$container: keepEnum
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
for (let i = 1; i < newEnumsGroup.length; i++) {
|
|
788
|
+
const idx = newModel.declarations.indexOf(newEnumsGroup[i]);
|
|
789
|
+
if (idx >= 0) newModel.declarations.splice(idx, 1);
|
|
790
|
+
}
|
|
791
|
+
for (const newDM of newDataModels) for (const field of newDM.fields) {
|
|
792
|
+
if (field.$type !== "DataField") continue;
|
|
793
|
+
const ref = field.type.reference?.ref;
|
|
794
|
+
if (ref && newEnumsGroup.includes(ref)) field.type.reference = {
|
|
795
|
+
ref: keepEnum,
|
|
796
|
+
$refText: keepEnum.name
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
console.log(colors.gray(`Consolidated enum${newEnumsGroup.length > 1 ? "s" : ""} ${newEnumsGroup.map((e) => e.name).join(", ")} → ${oldEnum.name}`));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/actions/pull/provider/mysql.ts
|
|
804
|
+
function normalizeGenerationExpression(typeDef) {
|
|
805
|
+
return typeDef.replace(/_([0-9A-Za-z_]+)\\?'/g, "'").replace(/\\'/g, "'");
|
|
806
|
+
}
|
|
807
|
+
const mysql = {
|
|
808
|
+
isSupportedFeature(feature) {
|
|
809
|
+
switch (feature) {
|
|
810
|
+
case "NativeEnum": return true;
|
|
811
|
+
default: return false;
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
getBuiltinType(type) {
|
|
815
|
+
const t = (type || "").toLowerCase().trim();
|
|
816
|
+
const isArray = false;
|
|
817
|
+
switch (t) {
|
|
818
|
+
case "tinyint":
|
|
819
|
+
case "smallint":
|
|
820
|
+
case "mediumint":
|
|
821
|
+
case "int":
|
|
822
|
+
case "integer": return {
|
|
823
|
+
type: "Int",
|
|
824
|
+
isArray
|
|
825
|
+
};
|
|
826
|
+
case "bigint": return {
|
|
827
|
+
type: "BigInt",
|
|
828
|
+
isArray
|
|
829
|
+
};
|
|
830
|
+
case "decimal":
|
|
831
|
+
case "numeric": return {
|
|
832
|
+
type: "Decimal",
|
|
833
|
+
isArray
|
|
834
|
+
};
|
|
835
|
+
case "float":
|
|
836
|
+
case "double":
|
|
837
|
+
case "real": return {
|
|
838
|
+
type: "Float",
|
|
839
|
+
isArray
|
|
840
|
+
};
|
|
841
|
+
case "boolean":
|
|
842
|
+
case "bool": return {
|
|
843
|
+
type: "Boolean",
|
|
844
|
+
isArray
|
|
845
|
+
};
|
|
846
|
+
case "char":
|
|
847
|
+
case "varchar":
|
|
848
|
+
case "tinytext":
|
|
849
|
+
case "text":
|
|
850
|
+
case "mediumtext":
|
|
851
|
+
case "longtext": return {
|
|
852
|
+
type: "String",
|
|
853
|
+
isArray
|
|
854
|
+
};
|
|
855
|
+
case "date":
|
|
856
|
+
case "time":
|
|
857
|
+
case "datetime":
|
|
858
|
+
case "timestamp":
|
|
859
|
+
case "year": return {
|
|
860
|
+
type: "DateTime",
|
|
861
|
+
isArray
|
|
862
|
+
};
|
|
863
|
+
case "binary":
|
|
864
|
+
case "varbinary":
|
|
865
|
+
case "tinyblob":
|
|
866
|
+
case "blob":
|
|
867
|
+
case "mediumblob":
|
|
868
|
+
case "longblob": return {
|
|
869
|
+
type: "Bytes",
|
|
870
|
+
isArray
|
|
871
|
+
};
|
|
872
|
+
case "json": return {
|
|
873
|
+
type: "Json",
|
|
874
|
+
isArray
|
|
875
|
+
};
|
|
876
|
+
default:
|
|
877
|
+
if (t.startsWith("enum(")) return {
|
|
878
|
+
type: "String",
|
|
879
|
+
isArray
|
|
880
|
+
};
|
|
881
|
+
if (t.startsWith("set(")) return {
|
|
882
|
+
type: "String",
|
|
883
|
+
isArray
|
|
884
|
+
};
|
|
885
|
+
return {
|
|
886
|
+
type: "Unsupported",
|
|
887
|
+
isArray
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
getDefaultDatabaseType(type) {
|
|
892
|
+
switch (type) {
|
|
893
|
+
case "String": return {
|
|
894
|
+
type: "varchar",
|
|
895
|
+
precision: 191
|
|
896
|
+
};
|
|
897
|
+
case "Boolean": return { type: "boolean" };
|
|
898
|
+
case "Int": return { type: "int" };
|
|
899
|
+
case "BigInt": return { type: "bigint" };
|
|
900
|
+
case "Float": return { type: "double" };
|
|
901
|
+
case "Decimal": return {
|
|
902
|
+
type: "decimal",
|
|
903
|
+
precision: 65
|
|
904
|
+
};
|
|
905
|
+
case "DateTime": return {
|
|
906
|
+
type: "datetime",
|
|
907
|
+
precision: 3
|
|
908
|
+
};
|
|
909
|
+
case "Json": return { type: "json" };
|
|
910
|
+
case "Bytes": return { type: "longblob" };
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
async introspect(connectionString, options) {
|
|
914
|
+
const connection = await (await import("mysql2/promise")).createConnection(connectionString);
|
|
915
|
+
try {
|
|
916
|
+
const databaseName = new URL(connectionString).pathname.replace("/", "");
|
|
917
|
+
if (!databaseName) throw new CliError("Database name not found in connection string");
|
|
918
|
+
const [tableRows] = await connection.execute(getTableIntrospectionQuery(), [databaseName]);
|
|
919
|
+
const tables = [];
|
|
920
|
+
for (const row of tableRows) {
|
|
921
|
+
const columns = typeof row.columns === "string" ? JSON.parse(row.columns) : row.columns;
|
|
922
|
+
const indexes = typeof row.indexes === "string" ? JSON.parse(row.indexes) : row.indexes;
|
|
923
|
+
const sortedColumns = (columns || []).sort((a, b) => (a.ordinal_position ?? 0) - (b.ordinal_position ?? 0)).map((col) => {
|
|
924
|
+
if (col.datatype === "enum" && col.datatype_name) return {
|
|
925
|
+
...col,
|
|
926
|
+
datatype_name: resolveNameCasing(options.modelCasing, col.datatype_name).name
|
|
927
|
+
};
|
|
928
|
+
if (col.computed && typeof col.datatype === "string") return {
|
|
929
|
+
...col,
|
|
930
|
+
datatype: normalizeGenerationExpression(col.datatype)
|
|
931
|
+
};
|
|
932
|
+
return col;
|
|
933
|
+
});
|
|
934
|
+
const filteredIndexes = (indexes || []).filter((idx) => !(idx.columns.length === 1 && idx.name === `${row.name}_${idx.columns[0]?.name}_fkey`));
|
|
935
|
+
tables.push({
|
|
936
|
+
schema: "",
|
|
937
|
+
name: row.name,
|
|
938
|
+
type: row.type,
|
|
939
|
+
definition: row.definition,
|
|
940
|
+
columns: sortedColumns,
|
|
941
|
+
indexes: filteredIndexes
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
const [enumRows] = await connection.execute(getEnumIntrospectionQuery(), [databaseName]);
|
|
945
|
+
return {
|
|
946
|
+
tables,
|
|
947
|
+
enums: enumRows.map((row) => {
|
|
948
|
+
const values = parseEnumValues(row.column_type);
|
|
949
|
+
const syntheticName = `${row.table_name}_${row.column_name}`;
|
|
950
|
+
const { name } = resolveNameCasing(options.modelCasing, syntheticName);
|
|
951
|
+
return {
|
|
952
|
+
schema_name: "",
|
|
953
|
+
enum_type: name,
|
|
954
|
+
values
|
|
955
|
+
};
|
|
956
|
+
})
|
|
957
|
+
};
|
|
958
|
+
} finally {
|
|
959
|
+
await connection.end();
|
|
960
|
+
}
|
|
961
|
+
},
|
|
962
|
+
getDefaultValue({ defaultValue, fieldType, datatype, datatype_name, services, enums }) {
|
|
963
|
+
const val = defaultValue.trim();
|
|
964
|
+
if (val.toUpperCase() === "NULL") return null;
|
|
965
|
+
if (datatype === "enum" && datatype_name) {
|
|
966
|
+
const enumDef = enums.find((e) => getDbName(e) === datatype_name);
|
|
967
|
+
if (enumDef) {
|
|
968
|
+
const enumValue = val.startsWith("'") && val.endsWith("'") ? val.slice(1, -1) : val;
|
|
969
|
+
const enumField = enumDef.fields.find((f) => getDbName(f) === enumValue);
|
|
970
|
+
if (enumField) return (ab) => ab.ReferenceExpr.setTarget(enumField);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
switch (fieldType) {
|
|
974
|
+
case "DateTime":
|
|
975
|
+
if (/^CURRENT_TIMESTAMP(\(\d*\))?$/i.test(val) || val.toLowerCase() === "current_timestamp()" || val.toLowerCase() === "now()") return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("now", services));
|
|
976
|
+
return (ab) => ab.StringLiteral.setValue(val);
|
|
977
|
+
case "Int":
|
|
978
|
+
case "BigInt":
|
|
979
|
+
if (val.toLowerCase() === "auto_increment") return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("autoincrement", services));
|
|
980
|
+
return (ab) => ab.NumberLiteral.setValue(val);
|
|
981
|
+
case "Float": return normalizeFloatDefault(val);
|
|
982
|
+
case "Decimal": return normalizeDecimalDefault(val);
|
|
983
|
+
case "Boolean": return (ab) => ab.BooleanLiteral.setValue(val.toLowerCase() === "true" || val === "1" || val === "b'1'");
|
|
984
|
+
case "String":
|
|
985
|
+
if (val.toLowerCase() === "uuid()") return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("uuid", services));
|
|
986
|
+
return (ab) => ab.StringLiteral.setValue(val);
|
|
987
|
+
case "Json": return (ab) => ab.StringLiteral.setValue(val);
|
|
988
|
+
case "Bytes": return (ab) => ab.StringLiteral.setValue(val);
|
|
989
|
+
}
|
|
990
|
+
if (val.includes("(") && val.includes(")")) return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("dbgenerated", services)).addArg((a) => a.setValue((v) => v.StringLiteral.setValue(val)));
|
|
991
|
+
console.warn(`Unsupported default value type: "${defaultValue}" for field type "${fieldType}". Skipping default value.`);
|
|
992
|
+
return null;
|
|
993
|
+
},
|
|
994
|
+
getFieldAttributes({ fieldName, fieldType, datatype, length, precision, services }) {
|
|
995
|
+
const factories = [];
|
|
996
|
+
if (fieldType === "DateTime" && (fieldName.toLowerCase() === "updatedat" || fieldName.toLowerCase() === "updated_at")) factories.push(new DataFieldAttributeFactory().setDecl(getAttributeRef("@updatedAt", services)));
|
|
997
|
+
const dbAttr = services.shared.workspace.IndexManager.allElements("Attribute").find((d) => d.name.toLowerCase() === `@db.${datatype.toLowerCase()}`)?.node;
|
|
998
|
+
const defaultDatabaseType = this.getDefaultDatabaseType(fieldType);
|
|
999
|
+
if (dbAttr && defaultDatabaseType && (defaultDatabaseType.type !== datatype || defaultDatabaseType.precision && defaultDatabaseType.precision !== (length ?? precision))) {
|
|
1000
|
+
const dbAttrFactory = new DataFieldAttributeFactory().setDecl(dbAttr);
|
|
1001
|
+
const sizeValue = length ?? precision;
|
|
1002
|
+
if (sizeValue !== void 0 && sizeValue !== null) dbAttrFactory.addArg((a) => a.NumberLiteral.setValue(sizeValue));
|
|
1003
|
+
factories.push(dbAttrFactory);
|
|
1004
|
+
}
|
|
1005
|
+
return factories;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
function getTableIntrospectionQuery() {
|
|
1009
|
+
return `
|
|
1010
|
+
-- Main query: one row per table/view with columns and indexes as nested JSON arrays.
|
|
1011
|
+
-- Uses INFORMATION_SCHEMA which is MySQL's standard metadata catalog.
|
|
1012
|
+
SELECT
|
|
1013
|
+
t.TABLE_NAME AS \`name\`, -- table or view name
|
|
1014
|
+
CASE t.TABLE_TYPE -- map MySQL table type strings to our internal types
|
|
1015
|
+
WHEN 'BASE TABLE' THEN 'table'
|
|
1016
|
+
WHEN 'VIEW' THEN 'view'
|
|
1017
|
+
ELSE NULL
|
|
1018
|
+
END AS \`type\`,
|
|
1019
|
+
CASE -- for views, retrieve the SQL definition
|
|
1020
|
+
WHEN t.TABLE_TYPE = 'VIEW' THEN v.VIEW_DEFINITION
|
|
1021
|
+
ELSE NULL
|
|
1022
|
+
END AS \`definition\`,
|
|
1023
|
+
|
|
1024
|
+
-- ===== COLUMNS subquery =====
|
|
1025
|
+
-- Wraps an ordered subquery in JSON_ARRAYAGG to produce a JSON array of column objects.
|
|
1026
|
+
(
|
|
1027
|
+
SELECT JSON_ARRAYAGG(col_json)
|
|
1028
|
+
FROM (
|
|
1029
|
+
SELECT JSON_OBJECT(
|
|
1030
|
+
'ordinal_position', c.ORDINAL_POSITION, -- column position (used for sorting)
|
|
1031
|
+
'name', c.COLUMN_NAME, -- column name
|
|
1032
|
+
|
|
1033
|
+
-- datatype: for generated/computed columns, construct the full DDL-like type definition
|
|
1034
|
+
-- (e.g., "int GENERATED ALWAYS AS (col1 + col2) STORED") so it can be rendered as
|
|
1035
|
+
-- Unsupported("..."); special-case tinyint(1) as 'boolean' (MySQL's boolean convention);
|
|
1036
|
+
-- otherwise use the DATA_TYPE (e.g., 'int', 'varchar', 'datetime').
|
|
1037
|
+
'datatype', CASE
|
|
1038
|
+
WHEN c.GENERATION_EXPRESSION IS NOT NULL AND c.GENERATION_EXPRESSION != '' THEN
|
|
1039
|
+
CONCAT(
|
|
1040
|
+
c.COLUMN_TYPE,
|
|
1041
|
+
' GENERATED ALWAYS AS (',
|
|
1042
|
+
c.GENERATION_EXPRESSION,
|
|
1043
|
+
') ',
|
|
1044
|
+
CASE
|
|
1045
|
+
WHEN c.EXTRA LIKE '%STORED GENERATED%' THEN 'STORED'
|
|
1046
|
+
ELSE 'VIRTUAL'
|
|
1047
|
+
END
|
|
1048
|
+
)
|
|
1049
|
+
WHEN c.DATA_TYPE = 'tinyint' AND c.COLUMN_TYPE = 'tinyint(1)' THEN 'boolean'
|
|
1050
|
+
ELSE c.DATA_TYPE
|
|
1051
|
+
END,
|
|
1052
|
+
|
|
1053
|
+
-- datatype_name: for enum columns, generate a synthetic name "TableName_ColumnName"
|
|
1054
|
+
-- (MySQL doesn't have named enum types like PostgreSQL)
|
|
1055
|
+
'datatype_name', CASE
|
|
1056
|
+
WHEN c.DATA_TYPE = 'enum' THEN CONCAT(t.TABLE_NAME, '_', c.COLUMN_NAME)
|
|
1057
|
+
ELSE NULL
|
|
1058
|
+
END,
|
|
1059
|
+
|
|
1060
|
+
'datatype_schema', '', -- MySQL doesn't support multi-schema
|
|
1061
|
+
'length', c.CHARACTER_MAXIMUM_LENGTH, -- max length for string types (e.g., VARCHAR(255) -> 255)
|
|
1062
|
+
'precision', COALESCE(c.NUMERIC_PRECISION, c.DATETIME_PRECISION), -- numeric or datetime precision
|
|
1063
|
+
|
|
1064
|
+
'nullable', c.IS_NULLABLE = 'YES', -- true if column allows NULL
|
|
1065
|
+
|
|
1066
|
+
-- default: for auto_increment columns, report 'auto_increment' instead of NULL;
|
|
1067
|
+
-- otherwise use the COLUMN_DEFAULT value
|
|
1068
|
+
'default', CASE
|
|
1069
|
+
WHEN c.EXTRA LIKE '%auto_increment%' THEN 'auto_increment'
|
|
1070
|
+
ELSE c.COLUMN_DEFAULT
|
|
1071
|
+
END,
|
|
1072
|
+
|
|
1073
|
+
'pk', c.COLUMN_KEY = 'PRI', -- true if column is part of the primary key
|
|
1074
|
+
|
|
1075
|
+
-- unique: true if the column has a single-column unique index.
|
|
1076
|
+
-- COLUMN_KEY = 'UNI' covers most cases, but may not be set when the column
|
|
1077
|
+
-- also participates in other indexes (showing 'MUL' instead on some MySQL versions).
|
|
1078
|
+
-- Also check INFORMATION_SCHEMA.STATISTICS for single-column unique indexes
|
|
1079
|
+
-- (NON_UNIQUE = 0) to match the PostgreSQL introspection behavior.
|
|
1080
|
+
'unique', (
|
|
1081
|
+
c.COLUMN_KEY = 'UNI'
|
|
1082
|
+
OR EXISTS (
|
|
1083
|
+
SELECT 1
|
|
1084
|
+
FROM INFORMATION_SCHEMA.STATISTICS s_uni
|
|
1085
|
+
WHERE s_uni.TABLE_SCHEMA = c.TABLE_SCHEMA
|
|
1086
|
+
AND s_uni.TABLE_NAME = c.TABLE_NAME
|
|
1087
|
+
AND s_uni.COLUMN_NAME = c.COLUMN_NAME
|
|
1088
|
+
AND s_uni.NON_UNIQUE = 0
|
|
1089
|
+
AND s_uni.INDEX_NAME != 'PRIMARY'
|
|
1090
|
+
AND (
|
|
1091
|
+
SELECT COUNT(*)
|
|
1092
|
+
FROM INFORMATION_SCHEMA.STATISTICS s_cnt
|
|
1093
|
+
WHERE s_cnt.TABLE_SCHEMA = s_uni.TABLE_SCHEMA
|
|
1094
|
+
AND s_cnt.TABLE_NAME = s_uni.TABLE_NAME
|
|
1095
|
+
AND s_cnt.INDEX_NAME = s_uni.INDEX_NAME
|
|
1096
|
+
) = 1
|
|
1097
|
+
)
|
|
1098
|
+
),
|
|
1099
|
+
'unique_name', (
|
|
1100
|
+
SELECT COALESCE(
|
|
1101
|
+
CASE WHEN c.COLUMN_KEY = 'UNI' THEN c.COLUMN_NAME ELSE NULL END,
|
|
1102
|
+
(
|
|
1103
|
+
SELECT s_uni.INDEX_NAME
|
|
1104
|
+
FROM INFORMATION_SCHEMA.STATISTICS s_uni
|
|
1105
|
+
WHERE s_uni.TABLE_SCHEMA = c.TABLE_SCHEMA
|
|
1106
|
+
AND s_uni.TABLE_NAME = c.TABLE_NAME
|
|
1107
|
+
AND s_uni.COLUMN_NAME = c.COLUMN_NAME
|
|
1108
|
+
AND s_uni.NON_UNIQUE = 0
|
|
1109
|
+
AND s_uni.INDEX_NAME != 'PRIMARY'
|
|
1110
|
+
AND (
|
|
1111
|
+
SELECT COUNT(*)
|
|
1112
|
+
FROM INFORMATION_SCHEMA.STATISTICS s_cnt
|
|
1113
|
+
WHERE s_cnt.TABLE_SCHEMA = s_uni.TABLE_SCHEMA
|
|
1114
|
+
AND s_cnt.TABLE_NAME = s_uni.TABLE_NAME
|
|
1115
|
+
AND s_cnt.INDEX_NAME = s_uni.INDEX_NAME
|
|
1116
|
+
) = 1
|
|
1117
|
+
LIMIT 1
|
|
1118
|
+
)
|
|
1119
|
+
)
|
|
1120
|
+
),
|
|
1121
|
+
|
|
1122
|
+
-- computed: true if column has a generation expression (virtual or stored)
|
|
1123
|
+
'computed', c.GENERATION_EXPRESSION IS NOT NULL AND c.GENERATION_EXPRESSION != '',
|
|
1124
|
+
|
|
1125
|
+
-- options: for enum columns, the full COLUMN_TYPE string (e.g., "enum('a','b','c')")
|
|
1126
|
+
-- which gets parsed into individual values later
|
|
1127
|
+
'options', CASE
|
|
1128
|
+
WHEN c.DATA_TYPE = 'enum' THEN c.COLUMN_TYPE
|
|
1129
|
+
ELSE NULL
|
|
1130
|
+
END,
|
|
1131
|
+
|
|
1132
|
+
-- Foreign key info (NULL if column is not part of a FK)
|
|
1133
|
+
'foreign_key_schema', NULL, -- MySQL doesn't support cross-schema FKs here
|
|
1134
|
+
'foreign_key_table', kcu_fk.REFERENCED_TABLE_NAME, -- referenced table
|
|
1135
|
+
'foreign_key_column', kcu_fk.REFERENCED_COLUMN_NAME, -- referenced column
|
|
1136
|
+
'foreign_key_name', kcu_fk.CONSTRAINT_NAME, -- FK constraint name
|
|
1137
|
+
'foreign_key_on_update', rc.UPDATE_RULE, -- referential action on update (CASCADE, SET NULL, etc.)
|
|
1138
|
+
'foreign_key_on_delete', rc.DELETE_RULE -- referential action on delete
|
|
1139
|
+
) AS col_json
|
|
1140
|
+
|
|
1141
|
+
FROM INFORMATION_SCHEMA.COLUMNS c -- one row per column in the database
|
|
1142
|
+
|
|
1143
|
+
-- Join KEY_COLUMN_USAGE to find foreign key references for this column.
|
|
1144
|
+
-- Filter to only FK entries (REFERENCED_TABLE_NAME IS NOT NULL).
|
|
1145
|
+
LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu_fk
|
|
1146
|
+
ON c.TABLE_SCHEMA = kcu_fk.TABLE_SCHEMA
|
|
1147
|
+
AND c.TABLE_NAME = kcu_fk.TABLE_NAME
|
|
1148
|
+
AND c.COLUMN_NAME = kcu_fk.COLUMN_NAME
|
|
1149
|
+
AND kcu_fk.REFERENCED_TABLE_NAME IS NOT NULL
|
|
1150
|
+
|
|
1151
|
+
-- Join REFERENTIAL_CONSTRAINTS to get ON UPDATE / ON DELETE rules for the FK.
|
|
1152
|
+
LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
|
|
1153
|
+
ON kcu_fk.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
|
1154
|
+
AND kcu_fk.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
|
1155
|
+
|
|
1156
|
+
WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA
|
|
1157
|
+
AND c.TABLE_NAME = t.TABLE_NAME
|
|
1158
|
+
ORDER BY c.ORDINAL_POSITION -- preserve original column order
|
|
1159
|
+
) AS cols_ordered
|
|
1160
|
+
) AS \`columns\`,
|
|
1161
|
+
|
|
1162
|
+
-- ===== INDEXES subquery =====
|
|
1163
|
+
-- Aggregates all indexes for this table into a JSON array.
|
|
1164
|
+
(
|
|
1165
|
+
SELECT JSON_ARRAYAGG(idx_json)
|
|
1166
|
+
FROM (
|
|
1167
|
+
SELECT JSON_OBJECT(
|
|
1168
|
+
'name', s.INDEX_NAME, -- index name (e.g., 'PRIMARY', 'idx_email')
|
|
1169
|
+
'method', s.INDEX_TYPE, -- index type (e.g., 'BTREE', 'HASH', 'FULLTEXT')
|
|
1170
|
+
'unique', s.NON_UNIQUE = 0, -- NON_UNIQUE=0 means it IS unique
|
|
1171
|
+
'primary', s.INDEX_NAME = 'PRIMARY', -- MySQL names the PK index 'PRIMARY'
|
|
1172
|
+
'valid', TRUE, -- MySQL doesn't expose index validity status
|
|
1173
|
+
'ready', TRUE, -- MySQL doesn't expose index readiness status
|
|
1174
|
+
'partial', FALSE, -- MySQL doesn't support partial indexes
|
|
1175
|
+
'predicate', NULL, -- no WHERE clause on indexes in MySQL
|
|
1176
|
+
|
|
1177
|
+
-- Index columns: nested subquery for columns in this index
|
|
1178
|
+
'columns', (
|
|
1179
|
+
SELECT JSON_ARRAYAGG(idx_col_json)
|
|
1180
|
+
FROM (
|
|
1181
|
+
SELECT JSON_OBJECT(
|
|
1182
|
+
'name', s2.COLUMN_NAME, -- column name in the index
|
|
1183
|
+
'expression', NULL, -- MySQL doesn't expose expression indexes via STATISTICS
|
|
1184
|
+
-- COLLATION: 'A' = ascending, 'D' = descending, NULL = not sorted
|
|
1185
|
+
'order', CASE s2.COLLATION WHEN 'A' THEN 'ASC' WHEN 'D' THEN 'DESC' ELSE NULL END,
|
|
1186
|
+
'nulls', NULL -- MySQL doesn't expose NULLS FIRST/LAST
|
|
1187
|
+
) AS idx_col_json
|
|
1188
|
+
FROM INFORMATION_SCHEMA.STATISTICS s2 -- one row per column per index
|
|
1189
|
+
WHERE s2.TABLE_SCHEMA = s.TABLE_SCHEMA
|
|
1190
|
+
AND s2.TABLE_NAME = s.TABLE_NAME
|
|
1191
|
+
AND s2.INDEX_NAME = s.INDEX_NAME
|
|
1192
|
+
ORDER BY s2.SEQ_IN_INDEX -- preserve column order within the index
|
|
1193
|
+
) AS idx_cols_ordered
|
|
1194
|
+
)
|
|
1195
|
+
) AS idx_json
|
|
1196
|
+
FROM (
|
|
1197
|
+
-- Deduplicate: STATISTICS has one row per (index, column), but we need one row per index.
|
|
1198
|
+
-- DISTINCT on INDEX_NAME gives us one entry per index with its metadata.
|
|
1199
|
+
SELECT DISTINCT INDEX_NAME, INDEX_TYPE, NON_UNIQUE, TABLE_SCHEMA, TABLE_NAME
|
|
1200
|
+
FROM INFORMATION_SCHEMA.STATISTICS
|
|
1201
|
+
WHERE TABLE_SCHEMA = t.TABLE_SCHEMA AND TABLE_NAME = t.TABLE_NAME
|
|
1202
|
+
) s
|
|
1203
|
+
) AS idxs_ordered
|
|
1204
|
+
) AS \`indexes\`
|
|
1205
|
+
|
|
1206
|
+
-- === Main FROM: INFORMATION_SCHEMA.TABLES lists all tables and views ===
|
|
1207
|
+
FROM INFORMATION_SCHEMA.TABLES t
|
|
1208
|
+
-- Join VIEWS to get VIEW_DEFINITION for view tables
|
|
1209
|
+
LEFT JOIN INFORMATION_SCHEMA.VIEWS v
|
|
1210
|
+
ON t.TABLE_SCHEMA = v.TABLE_SCHEMA AND t.TABLE_NAME = v.TABLE_NAME
|
|
1211
|
+
WHERE t.TABLE_SCHEMA = ? -- only the target database
|
|
1212
|
+
AND t.TABLE_TYPE IN ('BASE TABLE', 'VIEW') -- exclude system tables like SYSTEM VIEW
|
|
1213
|
+
AND t.TABLE_NAME <> '_prisma_migrations' -- exclude Prisma migration tracking table
|
|
1214
|
+
ORDER BY t.TABLE_NAME;
|
|
1215
|
+
`;
|
|
1216
|
+
}
|
|
1217
|
+
function getEnumIntrospectionQuery() {
|
|
1218
|
+
return `
|
|
1219
|
+
SELECT
|
|
1220
|
+
c.TABLE_NAME AS table_name, -- table containing the enum column
|
|
1221
|
+
c.COLUMN_NAME AS column_name, -- column name
|
|
1222
|
+
c.COLUMN_TYPE AS column_type -- full type string including values (e.g., "enum('val1','val2')")
|
|
1223
|
+
FROM INFORMATION_SCHEMA.COLUMNS c
|
|
1224
|
+
WHERE c.TABLE_SCHEMA = ? -- only the target database
|
|
1225
|
+
AND c.DATA_TYPE = 'enum' -- only enum columns
|
|
1226
|
+
ORDER BY c.TABLE_NAME, c.COLUMN_NAME;
|
|
1227
|
+
`;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Parse enum values from MySQL COLUMN_TYPE string like "enum('val1','val2','val3')"
|
|
1231
|
+
*/
|
|
1232
|
+
function parseEnumValues(columnType) {
|
|
1233
|
+
const match = columnType.match(/^enum\((.+)\)$/i);
|
|
1234
|
+
if (!match || !match[1]) return [];
|
|
1235
|
+
const valuesString = match[1];
|
|
1236
|
+
const values = [];
|
|
1237
|
+
let current = "";
|
|
1238
|
+
let inQuote = false;
|
|
1239
|
+
let i = 0;
|
|
1240
|
+
while (i < valuesString.length) {
|
|
1241
|
+
const char = valuesString[i];
|
|
1242
|
+
if (char === "'" && !inQuote) {
|
|
1243
|
+
inQuote = true;
|
|
1244
|
+
i++;
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (char === "'" && inQuote) {
|
|
1248
|
+
if (valuesString[i + 1] === "'") {
|
|
1249
|
+
current += "'";
|
|
1250
|
+
i += 2;
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
values.push(current);
|
|
1254
|
+
current = "";
|
|
1255
|
+
inQuote = false;
|
|
1256
|
+
i++;
|
|
1257
|
+
while (i < valuesString.length && (valuesString[i] === "," || valuesString[i] === " ")) i++;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (inQuote) current += char;
|
|
1261
|
+
i++;
|
|
1262
|
+
}
|
|
1263
|
+
return values;
|
|
1264
|
+
}
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/actions/pull/provider/postgresql.ts
|
|
1267
|
+
/**
|
|
1268
|
+
* Maps PostgreSQL internal type names to their standard SQL names for comparison.
|
|
1269
|
+
* This is used to normalize type names when checking against default database types.
|
|
1270
|
+
*/
|
|
1271
|
+
const pgTypnameToStandard = {
|
|
1272
|
+
int2: "smallint",
|
|
1273
|
+
int4: "integer",
|
|
1274
|
+
int8: "bigint",
|
|
1275
|
+
float4: "real",
|
|
1276
|
+
float8: "double precision",
|
|
1277
|
+
bool: "boolean",
|
|
1278
|
+
bpchar: "character",
|
|
1279
|
+
numeric: "decimal"
|
|
1280
|
+
};
|
|
1281
|
+
/**
|
|
1282
|
+
* Standard bit widths for integer/float types that shouldn't be added as precision arguments.
|
|
1283
|
+
* PostgreSQL returns these as precision values, but they're implicit for the type.
|
|
1284
|
+
*/
|
|
1285
|
+
const standardTypePrecisions = {
|
|
1286
|
+
int2: 16,
|
|
1287
|
+
smallint: 16,
|
|
1288
|
+
int4: 32,
|
|
1289
|
+
integer: 32,
|
|
1290
|
+
int8: 64,
|
|
1291
|
+
bigint: 64,
|
|
1292
|
+
float4: 24,
|
|
1293
|
+
real: 24,
|
|
1294
|
+
float8: 53,
|
|
1295
|
+
"double precision": 53
|
|
1296
|
+
};
|
|
1297
|
+
/**
|
|
1298
|
+
* Maps PostgreSQL typnames (from pg_type.typname) to ZenStack native type attribute names.
|
|
1299
|
+
* PostgreSQL introspection returns internal type names like 'int2', 'int4', 'float8', 'bpchar',
|
|
1300
|
+
* but ZenStack attributes are named @db.SmallInt, @db.Integer, @db.DoublePrecision, @db.Char, etc.
|
|
1301
|
+
*/
|
|
1302
|
+
const pgTypnameToZenStackNativeType = {
|
|
1303
|
+
int2: "SmallInt",
|
|
1304
|
+
smallint: "SmallInt",
|
|
1305
|
+
int4: "Integer",
|
|
1306
|
+
integer: "Integer",
|
|
1307
|
+
int8: "BigInt",
|
|
1308
|
+
bigint: "BigInt",
|
|
1309
|
+
numeric: "Decimal",
|
|
1310
|
+
decimal: "Decimal",
|
|
1311
|
+
float4: "Real",
|
|
1312
|
+
real: "Real",
|
|
1313
|
+
float8: "DoublePrecision",
|
|
1314
|
+
"double precision": "DoublePrecision",
|
|
1315
|
+
bool: "Boolean",
|
|
1316
|
+
boolean: "Boolean",
|
|
1317
|
+
text: "Text",
|
|
1318
|
+
varchar: "VarChar",
|
|
1319
|
+
"character varying": "VarChar",
|
|
1320
|
+
bpchar: "Char",
|
|
1321
|
+
character: "Char",
|
|
1322
|
+
uuid: "Uuid",
|
|
1323
|
+
date: "Date",
|
|
1324
|
+
time: "Time",
|
|
1325
|
+
timetz: "Timetz",
|
|
1326
|
+
timestamp: "Timestamp",
|
|
1327
|
+
timestamptz: "Timestamptz",
|
|
1328
|
+
bytea: "ByteA",
|
|
1329
|
+
json: "Json",
|
|
1330
|
+
jsonb: "JsonB",
|
|
1331
|
+
xml: "Xml",
|
|
1332
|
+
inet: "Inet",
|
|
1333
|
+
bit: "Bit",
|
|
1334
|
+
varbit: "VarBit",
|
|
1335
|
+
oid: "Oid",
|
|
1336
|
+
money: "Money",
|
|
1337
|
+
citext: "Citext"
|
|
1338
|
+
};
|
|
1339
|
+
const postgresql = {
|
|
1340
|
+
isSupportedFeature(feature) {
|
|
1341
|
+
return ["Schema", "NativeEnum"].includes(feature);
|
|
1342
|
+
},
|
|
1343
|
+
getBuiltinType(type) {
|
|
1344
|
+
const t = (type || "").toLowerCase();
|
|
1345
|
+
const isArray = t.startsWith("_");
|
|
1346
|
+
switch (t.replace(/^_/, "")) {
|
|
1347
|
+
case "int2":
|
|
1348
|
+
case "smallint":
|
|
1349
|
+
case "int4":
|
|
1350
|
+
case "integer": return {
|
|
1351
|
+
type: "Int",
|
|
1352
|
+
isArray
|
|
1353
|
+
};
|
|
1354
|
+
case "int8":
|
|
1355
|
+
case "bigint": return {
|
|
1356
|
+
type: "BigInt",
|
|
1357
|
+
isArray
|
|
1358
|
+
};
|
|
1359
|
+
case "numeric":
|
|
1360
|
+
case "decimal": return {
|
|
1361
|
+
type: "Decimal",
|
|
1362
|
+
isArray
|
|
1363
|
+
};
|
|
1364
|
+
case "float4":
|
|
1365
|
+
case "real":
|
|
1366
|
+
case "float8":
|
|
1367
|
+
case "double precision": return {
|
|
1368
|
+
type: "Float",
|
|
1369
|
+
isArray
|
|
1370
|
+
};
|
|
1371
|
+
case "bool":
|
|
1372
|
+
case "boolean": return {
|
|
1373
|
+
type: "Boolean",
|
|
1374
|
+
isArray
|
|
1375
|
+
};
|
|
1376
|
+
case "text":
|
|
1377
|
+
case "varchar":
|
|
1378
|
+
case "bpchar":
|
|
1379
|
+
case "character varying":
|
|
1380
|
+
case "character": return {
|
|
1381
|
+
type: "String",
|
|
1382
|
+
isArray
|
|
1383
|
+
};
|
|
1384
|
+
case "uuid": return {
|
|
1385
|
+
type: "String",
|
|
1386
|
+
isArray
|
|
1387
|
+
};
|
|
1388
|
+
case "date":
|
|
1389
|
+
case "time":
|
|
1390
|
+
case "timetz":
|
|
1391
|
+
case "timestamp":
|
|
1392
|
+
case "timestamptz": return {
|
|
1393
|
+
type: "DateTime",
|
|
1394
|
+
isArray
|
|
1395
|
+
};
|
|
1396
|
+
case "bytea": return {
|
|
1397
|
+
type: "Bytes",
|
|
1398
|
+
isArray
|
|
1399
|
+
};
|
|
1400
|
+
case "json":
|
|
1401
|
+
case "jsonb": return {
|
|
1402
|
+
type: "Json",
|
|
1403
|
+
isArray
|
|
1404
|
+
};
|
|
1405
|
+
default: return {
|
|
1406
|
+
type: "Unsupported",
|
|
1407
|
+
isArray
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
},
|
|
1411
|
+
async introspect(connectionString, options) {
|
|
1412
|
+
const { Client } = await import("pg");
|
|
1413
|
+
const client = new Client({ connectionString });
|
|
1414
|
+
await client.connect();
|
|
1415
|
+
try {
|
|
1416
|
+
const { rows: tables } = await client.query(tableIntrospectionQuery);
|
|
1417
|
+
const { rows: enums } = await client.query(enumIntrospectionQuery);
|
|
1418
|
+
const filteredTables = tables.filter((t) => options.schemas.includes(t.schema));
|
|
1419
|
+
return {
|
|
1420
|
+
enums: enums.filter((e) => options.schemas.includes(e.schema_name)),
|
|
1421
|
+
tables: filteredTables
|
|
1422
|
+
};
|
|
1423
|
+
} finally {
|
|
1424
|
+
await client.end();
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1427
|
+
getDefaultDatabaseType(type) {
|
|
1428
|
+
switch (type) {
|
|
1429
|
+
case "String": return { type: "text" };
|
|
1430
|
+
case "Boolean": return { type: "boolean" };
|
|
1431
|
+
case "Int": return { type: "integer" };
|
|
1432
|
+
case "BigInt": return { type: "bigint" };
|
|
1433
|
+
case "Float": return { type: "double precision" };
|
|
1434
|
+
case "Decimal": return { type: "decimal" };
|
|
1435
|
+
case "DateTime": return {
|
|
1436
|
+
type: "timestamp",
|
|
1437
|
+
precision: 3
|
|
1438
|
+
};
|
|
1439
|
+
case "Json": return { type: "jsonb" };
|
|
1440
|
+
case "Bytes": return { type: "bytea" };
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
getDefaultValue({ defaultValue, fieldType, datatype, datatype_name, services, enums }) {
|
|
1444
|
+
const val = defaultValue.trim();
|
|
1445
|
+
if (datatype === "enum" && datatype_name) {
|
|
1446
|
+
const enumDef = enums.find((e) => getDbName(e) === datatype_name);
|
|
1447
|
+
if (enumDef) {
|
|
1448
|
+
const enumValue = val.replace(/'/g, "").split("::")[0]?.trim();
|
|
1449
|
+
const enumField = enumDef.fields.find((f) => getDbName(f) === enumValue);
|
|
1450
|
+
if (enumField) return (ab) => ab.ReferenceExpr.setTarget(enumField);
|
|
1451
|
+
}
|
|
1452
|
+
return typeCastingConvert({
|
|
1453
|
+
defaultValue,
|
|
1454
|
+
enums,
|
|
1455
|
+
val,
|
|
1456
|
+
services
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
switch (fieldType) {
|
|
1460
|
+
case "DateTime":
|
|
1461
|
+
if (val === "CURRENT_TIMESTAMP" || val === "now()") return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("now", services));
|
|
1462
|
+
if (val.includes("::")) return typeCastingConvert({
|
|
1463
|
+
defaultValue,
|
|
1464
|
+
enums,
|
|
1465
|
+
val,
|
|
1466
|
+
services
|
|
1467
|
+
});
|
|
1468
|
+
return (ab) => ab.StringLiteral.setValue(val);
|
|
1469
|
+
case "Int":
|
|
1470
|
+
case "BigInt":
|
|
1471
|
+
if (val.startsWith("nextval(")) return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("autoincrement", services));
|
|
1472
|
+
if (val.includes("::")) return typeCastingConvert({
|
|
1473
|
+
defaultValue,
|
|
1474
|
+
enums,
|
|
1475
|
+
val,
|
|
1476
|
+
services
|
|
1477
|
+
});
|
|
1478
|
+
return (ab) => ab.NumberLiteral.setValue(val);
|
|
1479
|
+
case "Float":
|
|
1480
|
+
if (val.includes("::")) return typeCastingConvert({
|
|
1481
|
+
defaultValue,
|
|
1482
|
+
enums,
|
|
1483
|
+
val,
|
|
1484
|
+
services
|
|
1485
|
+
});
|
|
1486
|
+
return normalizeFloatDefault(val);
|
|
1487
|
+
case "Decimal":
|
|
1488
|
+
if (val.includes("::")) return typeCastingConvert({
|
|
1489
|
+
defaultValue,
|
|
1490
|
+
enums,
|
|
1491
|
+
val,
|
|
1492
|
+
services
|
|
1493
|
+
});
|
|
1494
|
+
return normalizeDecimalDefault(val);
|
|
1495
|
+
case "Boolean": return (ab) => ab.BooleanLiteral.setValue(val === "true");
|
|
1496
|
+
case "String":
|
|
1497
|
+
if (val.includes("::")) return typeCastingConvert({
|
|
1498
|
+
defaultValue,
|
|
1499
|
+
enums,
|
|
1500
|
+
val,
|
|
1501
|
+
services
|
|
1502
|
+
});
|
|
1503
|
+
if (val.startsWith("'") && val.endsWith("'")) return (ab) => ab.StringLiteral.setValue(val.slice(1, -1).replace(/''/g, "'"));
|
|
1504
|
+
return (ab) => ab.StringLiteral.setValue(val);
|
|
1505
|
+
case "Json":
|
|
1506
|
+
if (val.includes("::")) return typeCastingConvert({
|
|
1507
|
+
defaultValue,
|
|
1508
|
+
enums,
|
|
1509
|
+
val,
|
|
1510
|
+
services
|
|
1511
|
+
});
|
|
1512
|
+
return (ab) => ab.StringLiteral.setValue(val);
|
|
1513
|
+
case "Bytes":
|
|
1514
|
+
if (val.includes("::")) return typeCastingConvert({
|
|
1515
|
+
defaultValue,
|
|
1516
|
+
enums,
|
|
1517
|
+
val,
|
|
1518
|
+
services
|
|
1519
|
+
});
|
|
1520
|
+
return (ab) => ab.StringLiteral.setValue(val);
|
|
1521
|
+
}
|
|
1522
|
+
if (val.includes("(") && val.includes(")")) return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("dbgenerated", services)).addArg((a) => a.setValue((v) => v.StringLiteral.setValue(val)));
|
|
1523
|
+
console.warn(`Unsupported default value type: "${defaultValue}" for field type "${fieldType}". Skipping default value.`);
|
|
1524
|
+
return null;
|
|
1525
|
+
},
|
|
1526
|
+
getFieldAttributes({ fieldName, fieldType, datatype, length, precision, services }) {
|
|
1527
|
+
const factories = [];
|
|
1528
|
+
if (fieldType === "DateTime" && (fieldName.toLowerCase() === "updatedat" || fieldName.toLowerCase() === "updated_at")) factories.push(new DataFieldAttributeFactory().setDecl(getAttributeRef("@updatedAt", services)));
|
|
1529
|
+
const nativeTypeName = pgTypnameToZenStackNativeType[datatype.toLowerCase()] ?? datatype;
|
|
1530
|
+
const dbAttr = services.shared.workspace.IndexManager.allElements("Attribute").find((d) => d.name.toLowerCase() === `@db.${nativeTypeName.toLowerCase()}`)?.node;
|
|
1531
|
+
const defaultDatabaseType = this.getDefaultDatabaseType(fieldType);
|
|
1532
|
+
const normalizedDatatype = pgTypnameToStandard[datatype.toLowerCase()] ?? datatype.toLowerCase();
|
|
1533
|
+
const standardPrecision = standardTypePrecisions[datatype.toLowerCase()];
|
|
1534
|
+
const isStandardPrecision = standardPrecision !== void 0 && precision === standardPrecision;
|
|
1535
|
+
if (dbAttr && defaultDatabaseType && (defaultDatabaseType.type !== normalizedDatatype || defaultDatabaseType.precision && defaultDatabaseType.precision !== (length ?? precision))) {
|
|
1536
|
+
const dbAttrFactory = new DataFieldAttributeFactory().setDecl(dbAttr);
|
|
1537
|
+
if ((length || precision) && !isStandardPrecision) dbAttrFactory.addArg((a) => a.NumberLiteral.setValue(length || precision));
|
|
1538
|
+
factories.push(dbAttrFactory);
|
|
1539
|
+
}
|
|
1540
|
+
return factories;
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
const enumIntrospectionQuery = `
|
|
1544
|
+
SELECT
|
|
1545
|
+
n.nspname AS schema_name, -- schema the enum belongs to (e.g., 'public')
|
|
1546
|
+
t.typname AS enum_type, -- enum type name as defined in CREATE TYPE
|
|
1547
|
+
coalesce(json_agg(e.enumlabel ORDER BY e.enumsortorder), '[]') AS values -- ordered list of enum labels as JSON array
|
|
1548
|
+
FROM pg_type t -- pg_type: catalog of all data types
|
|
1549
|
+
JOIN pg_enum e ON t.oid = e.enumtypid -- pg_enum: one row per enum label; join to get labels for this enum type
|
|
1550
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace -- pg_namespace: schema info; join to get the schema name
|
|
1551
|
+
GROUP BY schema_name, enum_type -- one row per enum type, with all labels aggregated
|
|
1552
|
+
ORDER BY schema_name, enum_type;`;
|
|
1553
|
+
const tableIntrospectionQuery = `
|
|
1554
|
+
-- Main query: one row per table/view with columns and indexes as nested JSON arrays.
|
|
1555
|
+
-- Joins pg_class (tables/views) with pg_namespace (schemas).
|
|
1556
|
+
SELECT
|
|
1557
|
+
"ns"."nspname" AS "schema", -- schema name (e.g., 'public')
|
|
1558
|
+
"cls"."relname" AS "name", -- table or view name
|
|
1559
|
+
CASE "cls"."relkind" -- relkind: 'r' = ordinary table, 'v' = view
|
|
1560
|
+
WHEN 'r' THEN 'table'
|
|
1561
|
+
WHEN 'v' THEN 'view'
|
|
1562
|
+
ELSE NULL
|
|
1563
|
+
END AS "type",
|
|
1564
|
+
CASE -- for views, retrieve the SQL definition
|
|
1565
|
+
WHEN "cls"."relkind" = 'v' THEN pg_get_viewdef("cls"."oid", true)
|
|
1566
|
+
ELSE NULL
|
|
1567
|
+
END AS "definition",
|
|
1568
|
+
|
|
1569
|
+
-- ===== COLUMNS subquery =====
|
|
1570
|
+
-- Aggregates all columns for this table into a JSON array.
|
|
1571
|
+
(
|
|
1572
|
+
SELECT coalesce(json_agg(agg), '[]')
|
|
1573
|
+
FROM (
|
|
1574
|
+
SELECT
|
|
1575
|
+
"att"."attname" AS "name", -- column name
|
|
1576
|
+
|
|
1577
|
+
-- datatype: if the type is an enum, report 'enum';
|
|
1578
|
+
-- if the column is generated/computed, construct the full DDL-like type definition
|
|
1579
|
+
-- (e.g., "text GENERATED ALWAYS AS (expr) STORED") so it can be rendered as Unsupported("...");
|
|
1580
|
+
-- otherwise use the pg_type name.
|
|
1581
|
+
CASE
|
|
1582
|
+
WHEN EXISTS (
|
|
1583
|
+
SELECT 1 FROM "pg_catalog"."pg_enum" AS "e"
|
|
1584
|
+
WHERE "e"."enumtypid" = "typ"."oid"
|
|
1585
|
+
) THEN 'enum'
|
|
1586
|
+
WHEN "att"."attgenerated" != '' THEN
|
|
1587
|
+
format_type("att"."atttypid", "att"."atttypmod")
|
|
1588
|
+
|| ' GENERATED ALWAYS AS ('
|
|
1589
|
+
|| pg_get_expr("def"."adbin", "def"."adrelid")
|
|
1590
|
+
|| ') '
|
|
1591
|
+
|| CASE "att"."attgenerated"
|
|
1592
|
+
WHEN 's' THEN 'STORED'
|
|
1593
|
+
WHEN 'v' THEN 'VIRTUAL'
|
|
1594
|
+
ELSE 'STORED'
|
|
1595
|
+
END
|
|
1596
|
+
ELSE "typ"."typname"::text -- internal type name (e.g., 'int4', 'varchar', 'text'); cast to text to prevent CASE from coercing result to name type (max 63 chars)
|
|
1597
|
+
END AS "datatype",
|
|
1598
|
+
|
|
1599
|
+
-- datatype_name: for enums only, the actual enum type name (used to look up the enum definition)
|
|
1600
|
+
CASE
|
|
1601
|
+
WHEN EXISTS (
|
|
1602
|
+
SELECT 1 FROM "pg_catalog"."pg_enum" AS "e"
|
|
1603
|
+
WHERE "e"."enumtypid" = "typ"."oid"
|
|
1604
|
+
) THEN "typ"."typname"
|
|
1605
|
+
ELSE NULL
|
|
1606
|
+
END AS "datatype_name",
|
|
1607
|
+
|
|
1608
|
+
"tns"."nspname" AS "datatype_schema", -- schema where the data type is defined
|
|
1609
|
+
"c"."character_maximum_length" AS "length", -- max length for char/varchar types (from information_schema)
|
|
1610
|
+
COALESCE("c"."numeric_precision", "c"."datetime_precision") AS "precision", -- numeric or datetime precision
|
|
1611
|
+
|
|
1612
|
+
-- Foreign key info (NULL if column is not part of a FK constraint)
|
|
1613
|
+
"fk_ns"."nspname" AS "foreign_key_schema", -- schema of the referenced table
|
|
1614
|
+
"fk_cls"."relname" AS "foreign_key_table", -- referenced table name
|
|
1615
|
+
"fk_att"."attname" AS "foreign_key_column", -- referenced column name
|
|
1616
|
+
"fk_con"."conname" AS "foreign_key_name", -- FK constraint name
|
|
1617
|
+
|
|
1618
|
+
-- FK referential actions: decode single-char codes to human-readable strings
|
|
1619
|
+
CASE "fk_con"."confupdtype"
|
|
1620
|
+
WHEN 'a' THEN 'NO ACTION'
|
|
1621
|
+
WHEN 'r' THEN 'RESTRICT'
|
|
1622
|
+
WHEN 'c' THEN 'CASCADE'
|
|
1623
|
+
WHEN 'n' THEN 'SET NULL'
|
|
1624
|
+
WHEN 'd' THEN 'SET DEFAULT'
|
|
1625
|
+
ELSE NULL
|
|
1626
|
+
END AS "foreign_key_on_update",
|
|
1627
|
+
CASE "fk_con"."confdeltype"
|
|
1628
|
+
WHEN 'a' THEN 'NO ACTION'
|
|
1629
|
+
WHEN 'r' THEN 'RESTRICT'
|
|
1630
|
+
WHEN 'c' THEN 'CASCADE'
|
|
1631
|
+
WHEN 'n' THEN 'SET NULL'
|
|
1632
|
+
WHEN 'd' THEN 'SET DEFAULT'
|
|
1633
|
+
ELSE NULL
|
|
1634
|
+
END AS "foreign_key_on_delete",
|
|
1635
|
+
|
|
1636
|
+
-- pk: true if this column is part of the table's primary key constraint
|
|
1637
|
+
"pk_con"."conkey" IS NOT NULL AS "pk",
|
|
1638
|
+
|
|
1639
|
+
-- unique: true if the column has a single-column UNIQUE constraint OR a single-column unique index
|
|
1640
|
+
(
|
|
1641
|
+
-- Check for a single-column UNIQUE constraint (contype = 'u')
|
|
1642
|
+
EXISTS (
|
|
1643
|
+
SELECT 1
|
|
1644
|
+
FROM "pg_catalog"."pg_constraint" AS "u_con"
|
|
1645
|
+
WHERE "u_con"."contype" = 'u' -- 'u' = unique constraint
|
|
1646
|
+
AND "u_con"."conrelid" = "cls"."oid" -- on this table
|
|
1647
|
+
AND array_length("u_con"."conkey", 1) = 1 -- single-column only
|
|
1648
|
+
AND "att"."attnum" = ANY ("u_con"."conkey") -- this column is in the constraint
|
|
1649
|
+
)
|
|
1650
|
+
OR
|
|
1651
|
+
-- Check for a single-column unique index (may exist without an explicit constraint)
|
|
1652
|
+
EXISTS (
|
|
1653
|
+
SELECT 1
|
|
1654
|
+
FROM "pg_catalog"."pg_index" AS "u_idx"
|
|
1655
|
+
WHERE "u_idx"."indrelid" = "cls"."oid" -- on this table
|
|
1656
|
+
AND "u_idx"."indisunique" = TRUE -- it's a unique index
|
|
1657
|
+
AND "u_idx"."indnkeyatts" = 1 -- single key column
|
|
1658
|
+
AND "att"."attnum" = ANY ("u_idx"."indkey"::int2[]) -- this column is the key
|
|
1659
|
+
)
|
|
1660
|
+
) AS "unique",
|
|
1661
|
+
|
|
1662
|
+
-- unique_name: the name of the unique constraint or index (whichever exists first)
|
|
1663
|
+
(
|
|
1664
|
+
SELECT COALESCE(
|
|
1665
|
+
-- Try constraint name first
|
|
1666
|
+
(
|
|
1667
|
+
SELECT "u_con"."conname"
|
|
1668
|
+
FROM "pg_catalog"."pg_constraint" AS "u_con"
|
|
1669
|
+
WHERE "u_con"."contype" = 'u'
|
|
1670
|
+
AND "u_con"."conrelid" = "cls"."oid"
|
|
1671
|
+
AND array_length("u_con"."conkey", 1) = 1
|
|
1672
|
+
AND "att"."attnum" = ANY ("u_con"."conkey")
|
|
1673
|
+
LIMIT 1
|
|
1674
|
+
),
|
|
1675
|
+
-- Fall back to unique index name
|
|
1676
|
+
(
|
|
1677
|
+
SELECT "u_idx_cls"."relname"
|
|
1678
|
+
FROM "pg_catalog"."pg_index" AS "u_idx"
|
|
1679
|
+
JOIN "pg_catalog"."pg_class" AS "u_idx_cls" ON "u_idx"."indexrelid" = "u_idx_cls"."oid"
|
|
1680
|
+
WHERE "u_idx"."indrelid" = "cls"."oid"
|
|
1681
|
+
AND "u_idx"."indisunique" = TRUE
|
|
1682
|
+
AND "u_idx"."indnkeyatts" = 1
|
|
1683
|
+
AND "att"."attnum" = ANY ("u_idx"."indkey"::int2[])
|
|
1684
|
+
LIMIT 1
|
|
1685
|
+
)
|
|
1686
|
+
)
|
|
1687
|
+
) AS "unique_name",
|
|
1688
|
+
|
|
1689
|
+
"att"."attgenerated" != '' AS "computed", -- true if column is a generated/computed column
|
|
1690
|
+
-- For generated columns, pg_attrdef stores the generation expression (not a default),
|
|
1691
|
+
-- so we must null it out to avoid emitting a spurious @default(dbgenerated(...)) attribute.
|
|
1692
|
+
CASE
|
|
1693
|
+
WHEN "att"."attgenerated" != '' THEN NULL
|
|
1694
|
+
ELSE pg_get_expr("def"."adbin", "def"."adrelid")
|
|
1695
|
+
END AS "default", -- column default expression as text (e.g., 'nextval(...)', '0', 'now()')
|
|
1696
|
+
"att"."attnotnull" != TRUE AS "nullable", -- true if column allows NULL values
|
|
1697
|
+
|
|
1698
|
+
-- options: for enum columns, aggregates all allowed enum labels into a JSON array
|
|
1699
|
+
coalesce(
|
|
1700
|
+
(
|
|
1701
|
+
SELECT json_agg("enm"."enumlabel") AS "o"
|
|
1702
|
+
FROM "pg_catalog"."pg_enum" AS "enm"
|
|
1703
|
+
WHERE "enm"."enumtypid" = "typ"."oid"
|
|
1704
|
+
),
|
|
1705
|
+
'[]'
|
|
1706
|
+
) AS "options"
|
|
1707
|
+
|
|
1708
|
+
-- === FROM / JOINs for the columns subquery ===
|
|
1709
|
+
|
|
1710
|
+
-- pg_attribute: one row per table column (attnum >= 0 excludes system columns)
|
|
1711
|
+
FROM "pg_catalog"."pg_attribute" AS "att"
|
|
1712
|
+
|
|
1713
|
+
-- pg_type: data type of the column (e.g., int4, text, custom_enum)
|
|
1714
|
+
INNER JOIN "pg_catalog"."pg_type" AS "typ" ON "typ"."oid" = "att"."atttypid"
|
|
1715
|
+
|
|
1716
|
+
-- pg_namespace for the type: needed to determine which schema the type lives in
|
|
1717
|
+
INNER JOIN "pg_catalog"."pg_namespace" AS "tns" ON "tns"."oid" = "typ"."typnamespace"
|
|
1718
|
+
|
|
1719
|
+
-- information_schema.columns: provides length/precision info not easily available from pg_catalog
|
|
1720
|
+
LEFT JOIN "information_schema"."columns" AS "c" ON "c"."table_schema" = "ns"."nspname"
|
|
1721
|
+
AND "c"."table_name" = "cls"."relname"
|
|
1722
|
+
AND "c"."column_name" = "att"."attname"
|
|
1723
|
+
|
|
1724
|
+
-- pg_constraint (primary key): join on contype='p' to detect if column is part of PK
|
|
1725
|
+
LEFT JOIN "pg_catalog"."pg_constraint" AS "pk_con" ON "pk_con"."contype" = 'p'
|
|
1726
|
+
AND "pk_con"."conrelid" = "cls"."oid"
|
|
1727
|
+
AND "att"."attnum" = ANY ("pk_con"."conkey")
|
|
1728
|
+
|
|
1729
|
+
-- pg_constraint (foreign key): join on contype='f' to get FK details for this column
|
|
1730
|
+
LEFT JOIN "pg_catalog"."pg_constraint" AS "fk_con" ON "fk_con"."contype" = 'f'
|
|
1731
|
+
AND "fk_con"."conrelid" = "cls"."oid"
|
|
1732
|
+
AND "att"."attnum" = ANY ("fk_con"."conkey")
|
|
1733
|
+
|
|
1734
|
+
-- pg_class for FK target table: resolve the referenced table's OID to its name
|
|
1735
|
+
LEFT JOIN "pg_catalog"."pg_class" AS "fk_cls" ON "fk_cls"."oid" = "fk_con"."confrelid"
|
|
1736
|
+
|
|
1737
|
+
-- pg_namespace for FK target: get the schema of the referenced table
|
|
1738
|
+
LEFT JOIN "pg_catalog"."pg_namespace" AS "fk_ns" ON "fk_ns"."oid" = "fk_cls"."relnamespace"
|
|
1739
|
+
|
|
1740
|
+
-- pg_attribute for FK target column: resolve the referenced column number to its name.
|
|
1741
|
+
-- Use array_position to correlate by position: find this source column's index in conkey,
|
|
1742
|
+
-- then pick the referenced attnum at that same index from confkey.
|
|
1743
|
+
-- This ensures composite FKs correctly map each source column to its corresponding target column.
|
|
1744
|
+
LEFT JOIN "pg_catalog"."pg_attribute" AS "fk_att" ON "fk_att"."attrelid" = "fk_cls"."oid"
|
|
1745
|
+
AND "fk_att"."attnum" = "fk_con"."confkey"[array_position("fk_con"."conkey", "att"."attnum")]
|
|
1746
|
+
|
|
1747
|
+
-- pg_attrdef: column defaults; adbin contains the internal expression, decoded via pg_get_expr()
|
|
1748
|
+
LEFT JOIN "pg_catalog"."pg_attrdef" AS "def" ON "def"."adrelid" = "cls"."oid" AND "def"."adnum" = "att"."attnum"
|
|
1749
|
+
|
|
1750
|
+
WHERE
|
|
1751
|
+
"att"."attrelid" = "cls"."oid" -- only columns belonging to this table
|
|
1752
|
+
AND "att"."attnum" >= 0 -- exclude system columns (ctid, xmin, etc. have attnum < 0)
|
|
1753
|
+
AND "att"."attisdropped" != TRUE -- exclude dropped (deleted) columns
|
|
1754
|
+
ORDER BY "att"."attnum" -- preserve original column order
|
|
1755
|
+
) AS agg
|
|
1756
|
+
) AS "columns",
|
|
1757
|
+
|
|
1758
|
+
-- ===== INDEXES subquery =====
|
|
1759
|
+
-- Aggregates all indexes for this table into a JSON array.
|
|
1760
|
+
(
|
|
1761
|
+
SELECT coalesce(json_agg(agg), '[]')
|
|
1762
|
+
FROM (
|
|
1763
|
+
SELECT
|
|
1764
|
+
"idx_cls"."relname" AS "name", -- index name
|
|
1765
|
+
"am"."amname" AS "method", -- access method (e.g., 'btree', 'hash', 'gin', 'gist')
|
|
1766
|
+
"idx"."indisunique" AS "unique", -- true if unique index
|
|
1767
|
+
"idx"."indisprimary" AS "primary", -- true if this is the PK index
|
|
1768
|
+
"idx"."indisvalid" AS "valid", -- false during concurrent index builds
|
|
1769
|
+
"idx"."indisready" AS "ready", -- true when index is ready for inserts
|
|
1770
|
+
("idx"."indpred" IS NOT NULL) AS "partial", -- true if index has a WHERE clause (partial index)
|
|
1771
|
+
pg_get_expr("idx"."indpred", "idx"."indrelid") AS "predicate", -- the WHERE clause expression for partial indexes
|
|
1772
|
+
|
|
1773
|
+
-- Index columns: iterate over each position in the index key array
|
|
1774
|
+
(
|
|
1775
|
+
SELECT json_agg(
|
|
1776
|
+
json_build_object(
|
|
1777
|
+
-- 'name': column name, or for expression indexes the expression text
|
|
1778
|
+
'name', COALESCE("att"."attname", pg_get_indexdef("idx"."indexrelid", "s"."i", true)),
|
|
1779
|
+
-- 'expression': non-null only for expression-based index columns (e.g., lower(name))
|
|
1780
|
+
'expression', CASE WHEN "att"."attname" IS NULL THEN pg_get_indexdef("idx"."indexrelid", "s"."i", true) ELSE NULL END,
|
|
1781
|
+
-- 'order': sort direction; bit 0 of indoption = 1 means DESC
|
|
1782
|
+
'order', CASE ((( "idx"."indoption"::int2[] )["s"."i"] & 1)) WHEN 1 THEN 'DESC' ELSE 'ASC' END,
|
|
1783
|
+
-- 'nulls': null ordering; bit 1 of indoption = 1 means NULLS FIRST
|
|
1784
|
+
'nulls', CASE (((( "idx"."indoption"::int2[] )["s"."i"] >> 1) & 1)) WHEN 1 THEN 'NULLS FIRST' ELSE 'NULLS LAST' END
|
|
1785
|
+
)
|
|
1786
|
+
ORDER BY "s"."i" -- preserve column order within the index
|
|
1787
|
+
)
|
|
1788
|
+
-- generate_subscripts creates one row per index key position (1-based)
|
|
1789
|
+
FROM generate_subscripts("idx"."indkey"::int2[], 1) AS "s"("i")
|
|
1790
|
+
-- Join to pg_attribute to resolve column numbers to names
|
|
1791
|
+
-- NULL attname means it's an expression index column
|
|
1792
|
+
LEFT JOIN "pg_catalog"."pg_attribute" AS "att"
|
|
1793
|
+
ON "att"."attrelid" = "cls"."oid"
|
|
1794
|
+
AND "att"."attnum" = ("idx"."indkey"::int2[])["s"."i"]
|
|
1795
|
+
) AS "columns"
|
|
1796
|
+
|
|
1797
|
+
FROM "pg_catalog"."pg_index" AS "idx" -- pg_index: one row per index
|
|
1798
|
+
JOIN "pg_catalog"."pg_class" AS "idx_cls" ON "idx"."indexrelid" = "idx_cls"."oid" -- index's own pg_class entry (for the name)
|
|
1799
|
+
JOIN "pg_catalog"."pg_am" AS "am" ON "idx_cls"."relam" = "am"."oid" -- access method catalog
|
|
1800
|
+
WHERE "idx"."indrelid" = "cls"."oid" -- only indexes on this table
|
|
1801
|
+
ORDER BY "idx_cls"."relname"
|
|
1802
|
+
) AS agg
|
|
1803
|
+
) AS "indexes"
|
|
1804
|
+
|
|
1805
|
+
-- === Main FROM: pg_class (tables and views) joined with pg_namespace (schemas) ===
|
|
1806
|
+
FROM "pg_catalog"."pg_class" AS "cls"
|
|
1807
|
+
INNER JOIN "pg_catalog"."pg_namespace" AS "ns" ON "cls"."relnamespace" = "ns"."oid"
|
|
1808
|
+
WHERE
|
|
1809
|
+
"ns"."nspname" !~ '^pg_' -- exclude PostgreSQL internal schemas (pg_catalog, pg_toast, etc.)
|
|
1810
|
+
AND "ns"."nspname" != 'information_schema' -- exclude the information_schema
|
|
1811
|
+
AND "cls"."relkind" IN ('r', 'v') -- only tables ('r') and views ('v')
|
|
1812
|
+
AND "cls"."relname" !~ '^pg_' -- exclude system tables starting with pg_
|
|
1813
|
+
AND "cls"."relname" !~ '_prisma_migrations' -- exclude Prisma migration tracking table
|
|
1814
|
+
ORDER BY "ns"."nspname", "cls"."relname" ASC;
|
|
1815
|
+
`;
|
|
1816
|
+
function typeCastingConvert({ defaultValue, enums, val, services }) {
|
|
1817
|
+
const [value, type] = val.replace(/'/g, "").split("::").map((s) => s.trim());
|
|
1818
|
+
switch (type) {
|
|
1819
|
+
case "character varying":
|
|
1820
|
+
case "uuid":
|
|
1821
|
+
case "json":
|
|
1822
|
+
case "jsonb":
|
|
1823
|
+
case "text":
|
|
1824
|
+
if (value === "NULL") return null;
|
|
1825
|
+
return (ab) => ab.StringLiteral.setValue(value);
|
|
1826
|
+
case "real": return (ab) => ab.NumberLiteral.setValue(value);
|
|
1827
|
+
default: {
|
|
1828
|
+
const enumDef = enums.find((e) => getDbName(e, true) === type);
|
|
1829
|
+
if (!enumDef) return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("dbgenerated", services)).addArg((a) => a.setValue((v) => v.StringLiteral.setValue(val)));
|
|
1830
|
+
const enumField = enumDef.fields.find((v) => getDbName(v) === value);
|
|
1831
|
+
if (!enumField) throw new CliError(`Enum value ${value} not found in enum ${type} for default value ${defaultValue}`);
|
|
1832
|
+
return (ab) => ab.ReferenceExpr.setTarget(enumField);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
//#endregion
|
|
1837
|
+
//#region src/actions/pull/provider/sqlite.ts
|
|
1838
|
+
const sqlite = {
|
|
1839
|
+
isSupportedFeature(feature) {
|
|
1840
|
+
switch (feature) {
|
|
1841
|
+
case "Schema": return false;
|
|
1842
|
+
case "NativeEnum": return false;
|
|
1843
|
+
default: return false;
|
|
1844
|
+
}
|
|
1845
|
+
},
|
|
1846
|
+
getBuiltinType(type) {
|
|
1847
|
+
const t = (type || "").toLowerCase().trim().replace(/\(.*\)$/, "").trim();
|
|
1848
|
+
const isArray = false;
|
|
1849
|
+
switch (t) {
|
|
1850
|
+
case "integer":
|
|
1851
|
+
case "int":
|
|
1852
|
+
case "tinyint":
|
|
1853
|
+
case "smallint":
|
|
1854
|
+
case "mediumint":
|
|
1855
|
+
case "int2":
|
|
1856
|
+
case "int8": return {
|
|
1857
|
+
type: "Int",
|
|
1858
|
+
isArray
|
|
1859
|
+
};
|
|
1860
|
+
case "bigint":
|
|
1861
|
+
case "unsigned big int": return {
|
|
1862
|
+
type: "BigInt",
|
|
1863
|
+
isArray
|
|
1864
|
+
};
|
|
1865
|
+
case "text":
|
|
1866
|
+
case "varchar":
|
|
1867
|
+
case "char":
|
|
1868
|
+
case "character":
|
|
1869
|
+
case "varying character":
|
|
1870
|
+
case "nchar":
|
|
1871
|
+
case "native character":
|
|
1872
|
+
case "nvarchar":
|
|
1873
|
+
case "clob": return {
|
|
1874
|
+
type: "String",
|
|
1875
|
+
isArray
|
|
1876
|
+
};
|
|
1877
|
+
case "blob": return {
|
|
1878
|
+
type: "Bytes",
|
|
1879
|
+
isArray
|
|
1880
|
+
};
|
|
1881
|
+
case "real":
|
|
1882
|
+
case "float":
|
|
1883
|
+
case "double":
|
|
1884
|
+
case "double precision": return {
|
|
1885
|
+
type: "Float",
|
|
1886
|
+
isArray
|
|
1887
|
+
};
|
|
1888
|
+
case "numeric":
|
|
1889
|
+
case "decimal": return {
|
|
1890
|
+
type: "Decimal",
|
|
1891
|
+
isArray
|
|
1892
|
+
};
|
|
1893
|
+
case "datetime":
|
|
1894
|
+
case "date":
|
|
1895
|
+
case "time":
|
|
1896
|
+
case "timestamp": return {
|
|
1897
|
+
type: "DateTime",
|
|
1898
|
+
isArray
|
|
1899
|
+
};
|
|
1900
|
+
case "json":
|
|
1901
|
+
case "jsonb": return {
|
|
1902
|
+
type: "Json",
|
|
1903
|
+
isArray
|
|
1904
|
+
};
|
|
1905
|
+
case "boolean":
|
|
1906
|
+
case "bool": return {
|
|
1907
|
+
type: "Boolean",
|
|
1908
|
+
isArray
|
|
1909
|
+
};
|
|
1910
|
+
default:
|
|
1911
|
+
if (!t) return {
|
|
1912
|
+
type: "Bytes",
|
|
1913
|
+
isArray
|
|
1914
|
+
};
|
|
1915
|
+
if (t.includes("int")) return {
|
|
1916
|
+
type: "Int",
|
|
1917
|
+
isArray
|
|
1918
|
+
};
|
|
1919
|
+
if (t.includes("char") || t.includes("clob") || t.includes("text")) return {
|
|
1920
|
+
type: "String",
|
|
1921
|
+
isArray
|
|
1922
|
+
};
|
|
1923
|
+
if (t.includes("blob")) return {
|
|
1924
|
+
type: "Bytes",
|
|
1925
|
+
isArray
|
|
1926
|
+
};
|
|
1927
|
+
if (t.includes("real") || t.includes("floa") || t.includes("doub")) return {
|
|
1928
|
+
type: "Float",
|
|
1929
|
+
isArray
|
|
1930
|
+
};
|
|
1931
|
+
return {
|
|
1932
|
+
type: "Unsupported",
|
|
1933
|
+
isArray
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
},
|
|
1937
|
+
getDefaultDatabaseType() {},
|
|
1938
|
+
async introspect(connectionString, _options) {
|
|
1939
|
+
const SQLite = (await import("better-sqlite3")).default;
|
|
1940
|
+
const db = new SQLite(connectionString, { readonly: true });
|
|
1941
|
+
try {
|
|
1942
|
+
const all = (sql) => {
|
|
1943
|
+
return db.prepare(sql).all();
|
|
1944
|
+
};
|
|
1945
|
+
const tablesRaw = all("SELECT name, type, sql AS definition FROM sqlite_schema WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%' AND name <> '_prisma_migrations' ORDER BY name");
|
|
1946
|
+
const autoIncrementTables = /* @__PURE__ */ new Set();
|
|
1947
|
+
for (const t of tablesRaw) if (t.type === "table" && t.definition) {
|
|
1948
|
+
if (/\bAUTOINCREMENT\b/i.test(t.definition)) autoIncrementTables.add(t.name);
|
|
1949
|
+
}
|
|
1950
|
+
const tables = [];
|
|
1951
|
+
for (const t of tablesRaw) {
|
|
1952
|
+
const tableName = t.name;
|
|
1953
|
+
const schema = "";
|
|
1954
|
+
const hasAutoIncrement = autoIncrementTables.has(tableName);
|
|
1955
|
+
const columnsInfo = all(`PRAGMA table_xinfo('${tableName.replace(/'/g, "''")}')`);
|
|
1956
|
+
const idxList = all(`PRAGMA index_list('${tableName.replace(/'/g, "''")}')`).filter((r) => !r.name.startsWith("sqlite_autoindex_"));
|
|
1957
|
+
const uniqueSingleColumn = /* @__PURE__ */ new Set();
|
|
1958
|
+
const uniqueIndexRows = idxList.filter((r) => r.unique === 1 && r.partial !== 1);
|
|
1959
|
+
for (const idx of uniqueIndexRows) {
|
|
1960
|
+
const idxCols = all(`PRAGMA index_info('${idx.name.replace(/'/g, "''")}')`);
|
|
1961
|
+
if (idxCols.length === 1 && idxCols[0]?.name) uniqueSingleColumn.add(idxCols[0].name);
|
|
1962
|
+
}
|
|
1963
|
+
const indexes = idxList.map((idx) => {
|
|
1964
|
+
const idxCols = all(`PRAGMA index_info('${idx.name.replace(/'/g, "''")}')`);
|
|
1965
|
+
return {
|
|
1966
|
+
name: idx.name,
|
|
1967
|
+
method: null,
|
|
1968
|
+
unique: idx.unique === 1,
|
|
1969
|
+
primary: false,
|
|
1970
|
+
valid: true,
|
|
1971
|
+
ready: true,
|
|
1972
|
+
partial: idx.partial === 1,
|
|
1973
|
+
predicate: idx.partial === 1 ? "[partial]" : null,
|
|
1974
|
+
columns: idxCols.map((col) => ({
|
|
1975
|
+
name: col.name,
|
|
1976
|
+
expression: null,
|
|
1977
|
+
order: null,
|
|
1978
|
+
nulls: null
|
|
1979
|
+
}))
|
|
1980
|
+
};
|
|
1981
|
+
});
|
|
1982
|
+
const fkRows = all(`PRAGMA foreign_key_list('${tableName.replace(/'/g, "''")}')`);
|
|
1983
|
+
const fkConstraintNames = /* @__PURE__ */ new Map();
|
|
1984
|
+
if (t.definition) {
|
|
1985
|
+
const fkRegex = /CONSTRAINT\s+(?:["'`]([^"'`]+)["'`]|(\w+))\s+FOREIGN\s+KEY\s*\(([^)]+)\)/gi;
|
|
1986
|
+
let match;
|
|
1987
|
+
while ((match = fkRegex.exec(t.definition)) !== null) {
|
|
1988
|
+
const constraintName = match[1] || match[2];
|
|
1989
|
+
const columnList = match[3];
|
|
1990
|
+
if (constraintName && columnList) {
|
|
1991
|
+
const columns = columnList.split(",").map((col) => col.trim().replace(/^["'`]|["'`]$/g, ""));
|
|
1992
|
+
for (const col of columns) if (col) fkConstraintNames.set(col, constraintName);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
const fkByColumn = /* @__PURE__ */ new Map();
|
|
1997
|
+
for (const fk of fkRows) fkByColumn.set(fk.from, {
|
|
1998
|
+
foreign_key_schema: "",
|
|
1999
|
+
foreign_key_table: fk.table || null,
|
|
2000
|
+
foreign_key_column: fk.to || null,
|
|
2001
|
+
foreign_key_name: fkConstraintNames.get(fk.from) ?? null,
|
|
2002
|
+
foreign_key_on_update: fk.on_update ?? null,
|
|
2003
|
+
foreign_key_on_delete: fk.on_delete ?? null
|
|
2004
|
+
});
|
|
2005
|
+
const generatedColDefs = t.definition ? extractColumnTypeDefs(t.definition) : /* @__PURE__ */ new Map();
|
|
2006
|
+
const columns = [];
|
|
2007
|
+
for (const c of columnsInfo) {
|
|
2008
|
+
const hidden = c.hidden ?? 0;
|
|
2009
|
+
if (hidden === 1) continue;
|
|
2010
|
+
const isGenerated = hidden === 2 || hidden === 3;
|
|
2011
|
+
const fk = fkByColumn.get(c.name);
|
|
2012
|
+
let defaultValue = c.dflt_value;
|
|
2013
|
+
if (hasAutoIncrement && c.pk) defaultValue = "autoincrement";
|
|
2014
|
+
let datatype = c.type || "";
|
|
2015
|
+
if (isGenerated) {
|
|
2016
|
+
const fullDef = generatedColDefs.get(c.name);
|
|
2017
|
+
if (fullDef) datatype = fullDef;
|
|
2018
|
+
}
|
|
2019
|
+
columns.push({
|
|
2020
|
+
name: c.name,
|
|
2021
|
+
datatype,
|
|
2022
|
+
datatype_name: null,
|
|
2023
|
+
length: null,
|
|
2024
|
+
precision: null,
|
|
2025
|
+
datatype_schema: schema,
|
|
2026
|
+
foreign_key_schema: fk?.foreign_key_schema ?? null,
|
|
2027
|
+
foreign_key_table: fk?.foreign_key_table ?? null,
|
|
2028
|
+
foreign_key_column: fk?.foreign_key_column ?? null,
|
|
2029
|
+
foreign_key_name: fk?.foreign_key_name ?? null,
|
|
2030
|
+
foreign_key_on_update: fk?.foreign_key_on_update ?? null,
|
|
2031
|
+
foreign_key_on_delete: fk?.foreign_key_on_delete ?? null,
|
|
2032
|
+
pk: !!c.pk,
|
|
2033
|
+
computed: isGenerated,
|
|
2034
|
+
nullable: c.notnull !== 1,
|
|
2035
|
+
default: defaultValue,
|
|
2036
|
+
unique: uniqueSingleColumn.has(c.name),
|
|
2037
|
+
unique_name: null
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
tables.push({
|
|
2041
|
+
schema,
|
|
2042
|
+
name: tableName,
|
|
2043
|
+
columns,
|
|
2044
|
+
type: t.type,
|
|
2045
|
+
definition: t.definition,
|
|
2046
|
+
indexes
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
return {
|
|
2050
|
+
tables,
|
|
2051
|
+
enums: []
|
|
2052
|
+
};
|
|
2053
|
+
} finally {
|
|
2054
|
+
db.close();
|
|
2055
|
+
}
|
|
2056
|
+
},
|
|
2057
|
+
getDefaultValue({ defaultValue, fieldType, services, enums }) {
|
|
2058
|
+
const val = defaultValue.trim();
|
|
2059
|
+
switch (fieldType) {
|
|
2060
|
+
case "DateTime":
|
|
2061
|
+
if (val === "CURRENT_TIMESTAMP" || val === "now()") return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("now", services));
|
|
2062
|
+
return (ab) => ab.StringLiteral.setValue(val);
|
|
2063
|
+
case "Int":
|
|
2064
|
+
case "BigInt":
|
|
2065
|
+
if (val === "autoincrement") return (ab) => ab.InvocationExpr.setFunction(getFunctionRef("autoincrement", services));
|
|
2066
|
+
return (ab) => ab.NumberLiteral.setValue(val);
|
|
2067
|
+
case "Float": return normalizeFloatDefault(val);
|
|
2068
|
+
case "Decimal": return normalizeDecimalDefault(val);
|
|
2069
|
+
case "Boolean": return (ab) => ab.BooleanLiteral.setValue(val === "true" || val === "1");
|
|
2070
|
+
case "String":
|
|
2071
|
+
if (val.startsWith("'") && val.endsWith("'")) {
|
|
2072
|
+
const strippedName = val.slice(1, -1);
|
|
2073
|
+
const enumDef = enums.find((e) => e.fields.find((v) => getDbName(v) === strippedName));
|
|
2074
|
+
if (enumDef) {
|
|
2075
|
+
const enumField = enumDef.fields.find((v) => getDbName(v) === strippedName);
|
|
2076
|
+
if (enumField) return (ab) => ab.ReferenceExpr.setTarget(enumField);
|
|
2077
|
+
}
|
|
2078
|
+
return (ab) => ab.StringLiteral.setValue(strippedName);
|
|
2079
|
+
}
|
|
2080
|
+
return (ab) => ab.StringLiteral.setValue(val);
|
|
2081
|
+
case "Json": return (ab) => ab.StringLiteral.setValue(val);
|
|
2082
|
+
case "Bytes": return (ab) => ab.StringLiteral.setValue(val);
|
|
2083
|
+
}
|
|
2084
|
+
console.warn(`Unsupported default value type: "${defaultValue}" for field type "${fieldType}". Skipping default value.`);
|
|
2085
|
+
return null;
|
|
2086
|
+
},
|
|
2087
|
+
getFieldAttributes({ fieldName, fieldType, services }) {
|
|
2088
|
+
const factories = [];
|
|
2089
|
+
if (fieldType === "DateTime" && (fieldName.toLowerCase() === "updatedat" || fieldName.toLowerCase() === "updated_at")) factories.push(new DataFieldAttributeFactory().setDecl(getAttributeRef("@updatedAt", services)));
|
|
2090
|
+
return factories;
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
/**
|
|
2094
|
+
* Extract column type definitions from a CREATE TABLE DDL statement.
|
|
2095
|
+
* Returns a map of column name → full type definition string (everything after the column name).
|
|
2096
|
+
* Used to get the complete type including GENERATED ALWAYS AS (...) STORED/VIRTUAL for generated columns.
|
|
2097
|
+
*/
|
|
2098
|
+
function extractColumnTypeDefs(ddl) {
|
|
2099
|
+
const openIdx = ddl.indexOf("(");
|
|
2100
|
+
if (openIdx === -1) return /* @__PURE__ */ new Map();
|
|
2101
|
+
let depth = 1;
|
|
2102
|
+
let closeIdx = -1;
|
|
2103
|
+
for (let i = openIdx + 1; i < ddl.length; i++) if (ddl[i] === "(") depth++;
|
|
2104
|
+
else if (ddl[i] === ")") {
|
|
2105
|
+
depth--;
|
|
2106
|
+
if (depth === 0) {
|
|
2107
|
+
closeIdx = i;
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
if (closeIdx === -1) return /* @__PURE__ */ new Map();
|
|
2112
|
+
const content = ddl.substring(openIdx + 1, closeIdx);
|
|
2113
|
+
const defs = [];
|
|
2114
|
+
let current = "";
|
|
2115
|
+
depth = 0;
|
|
2116
|
+
for (const char of content) {
|
|
2117
|
+
if (char === "(") depth++;
|
|
2118
|
+
else if (char === ")") depth--;
|
|
2119
|
+
else if (char === "," && depth === 0) {
|
|
2120
|
+
defs.push(current.trim());
|
|
2121
|
+
current = "";
|
|
2122
|
+
continue;
|
|
2123
|
+
}
|
|
2124
|
+
current += char;
|
|
2125
|
+
}
|
|
2126
|
+
if (current.trim()) defs.push(current.trim());
|
|
2127
|
+
const result = /* @__PURE__ */ new Map();
|
|
2128
|
+
for (const def of defs) {
|
|
2129
|
+
const nameMatch = def.match(/^(?:["'`]([^"'`]+)["'`]|(\w+))\s+(.+)/s);
|
|
2130
|
+
if (nameMatch) {
|
|
2131
|
+
const name = nameMatch[1] || nameMatch[2];
|
|
2132
|
+
const typeDef = nameMatch[3];
|
|
2133
|
+
if (name && typeDef) result.set(name, typeDef.trim());
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return result;
|
|
2137
|
+
}
|
|
2138
|
+
//#endregion
|
|
2139
|
+
//#region src/actions/pull/provider/index.ts
|
|
2140
|
+
const providers = {
|
|
2141
|
+
mysql,
|
|
2142
|
+
postgresql,
|
|
2143
|
+
sqlite
|
|
2144
|
+
};
|
|
2145
|
+
//#endregion
|
|
2146
|
+
//#region src/actions/db.ts
|
|
2147
|
+
/**
|
|
2148
|
+
* CLI action for db related commands
|
|
2149
|
+
*/
|
|
2150
|
+
async function run$7(command, options) {
|
|
2151
|
+
switch (command) {
|
|
2152
|
+
case "push":
|
|
2153
|
+
await runPush(options);
|
|
2154
|
+
break;
|
|
2155
|
+
case "pull":
|
|
2156
|
+
await runPull(options);
|
|
2157
|
+
break;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
async function runPush(options) {
|
|
2161
|
+
const schemaFile = getSchemaFile(options.schema);
|
|
2162
|
+
await requireDataSourceUrl(schemaFile);
|
|
2163
|
+
const prismaSchemaFile = await generateTempPrismaSchema(schemaFile);
|
|
2164
|
+
try {
|
|
2165
|
+
const cmd = [
|
|
2166
|
+
"db push",
|
|
2167
|
+
` --schema "${prismaSchemaFile}"`,
|
|
2168
|
+
options.acceptDataLoss ? " --accept-data-loss" : "",
|
|
2169
|
+
options.forceReset ? " --force-reset" : "",
|
|
2170
|
+
" --skip-generate"
|
|
2171
|
+
].join("");
|
|
2172
|
+
try {
|
|
2173
|
+
execPrisma(cmd);
|
|
2174
|
+
} catch (err) {
|
|
2175
|
+
handleSubProcessError$1(err);
|
|
2176
|
+
}
|
|
2177
|
+
} finally {
|
|
2178
|
+
if (fs.existsSync(prismaSchemaFile)) fs.unlinkSync(prismaSchemaFile);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
async function runPull(options) {
|
|
2182
|
+
const spinner = ora();
|
|
2183
|
+
try {
|
|
2184
|
+
const schemaFile = getSchemaFile(options.schema);
|
|
2185
|
+
const outPath = options.output ? path.resolve(options.output) : void 0;
|
|
2186
|
+
const treatAsFile = !!outPath && (fs.existsSync(outPath) && fs.lstatSync(outPath).isFile() || path.extname(outPath) !== "");
|
|
2187
|
+
const { model, services } = await loadSchemaDocument(schemaFile, {
|
|
2188
|
+
returnServices: true,
|
|
2189
|
+
mergeImports: treatAsFile
|
|
2190
|
+
});
|
|
2191
|
+
const SUPPORTED_PROVIDERS = Object.keys(providers);
|
|
2192
|
+
const datasource = getDatasource(model);
|
|
2193
|
+
if (!SUPPORTED_PROVIDERS.includes(datasource.provider)) throw new CliError(`Unsupported datasource provider: ${datasource.provider}`);
|
|
2194
|
+
const provider = providers[datasource.provider];
|
|
2195
|
+
if (!provider) throw new CliError(`No introspection provider found for: ${datasource.provider}`);
|
|
2196
|
+
spinner.start("Introspecting database...");
|
|
2197
|
+
const { enums, tables } = await provider.introspect(datasource.url, {
|
|
2198
|
+
schemas: datasource.allSchemas,
|
|
2199
|
+
modelCasing: options.modelCasing
|
|
2200
|
+
});
|
|
2201
|
+
spinner.succeed("Database introspected");
|
|
2202
|
+
console.log(colors.blue("Syncing schema..."));
|
|
2203
|
+
const newModel = {
|
|
2204
|
+
$type: "Model",
|
|
2205
|
+
$container: void 0,
|
|
2206
|
+
$containerProperty: void 0,
|
|
2207
|
+
$containerIndex: void 0,
|
|
2208
|
+
declarations: [...model.declarations.filter((d) => ["DataSource"].includes(d.$type))],
|
|
2209
|
+
imports: model.imports
|
|
2210
|
+
};
|
|
2211
|
+
syncEnums({
|
|
2212
|
+
dbEnums: enums,
|
|
2213
|
+
model: newModel,
|
|
2214
|
+
services,
|
|
2215
|
+
options,
|
|
2216
|
+
defaultSchema: datasource.defaultSchema,
|
|
2217
|
+
oldModel: model,
|
|
2218
|
+
provider
|
|
2219
|
+
});
|
|
2220
|
+
const resolvedRelations = [];
|
|
2221
|
+
for (const table of tables) {
|
|
2222
|
+
const relations = syncTable({
|
|
2223
|
+
table,
|
|
2224
|
+
model: newModel,
|
|
2225
|
+
provider,
|
|
2226
|
+
services,
|
|
2227
|
+
options,
|
|
2228
|
+
defaultSchema: datasource.defaultSchema,
|
|
2229
|
+
oldModel: model
|
|
2230
|
+
});
|
|
2231
|
+
resolvedRelations.push(...relations);
|
|
2232
|
+
}
|
|
2233
|
+
for (const relation of resolvedRelations) {
|
|
2234
|
+
const similarRelations = resolvedRelations.filter((rr) => {
|
|
2235
|
+
return rr !== relation && (rr.schema === relation.schema && rr.table === relation.table && rr.references.schema === relation.references.schema && rr.references.table === relation.references.table || rr.schema === relation.references.schema && rr.columns[0] === relation.references.columns[0] && rr.references.schema === relation.schema && rr.references.table === relation.table);
|
|
2236
|
+
}).length;
|
|
2237
|
+
syncRelation({
|
|
2238
|
+
model: newModel,
|
|
2239
|
+
relation,
|
|
2240
|
+
services,
|
|
2241
|
+
options,
|
|
2242
|
+
selfRelation: relation.references.schema === relation.schema && relation.references.table === relation.table,
|
|
2243
|
+
similarRelations
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
consolidateEnums({
|
|
2247
|
+
newModel,
|
|
2248
|
+
oldModel: model
|
|
2249
|
+
});
|
|
2250
|
+
console.log(colors.blue("Schema synced"));
|
|
2251
|
+
const baseDir = path.dirname(path.resolve(schemaFile));
|
|
2252
|
+
const baseDirUrlPath = new URL(`file://${baseDir}`).pathname;
|
|
2253
|
+
const docs = services.shared.workspace.LangiumDocuments.all.filter(({ uri }) => uri.path.toLowerCase().startsWith(baseDirUrlPath.toLowerCase())).toArray();
|
|
2254
|
+
const docsSet = new Set(docs.map((d) => d.uri.toString()));
|
|
2255
|
+
console.log(colors.bold("\nApplying changes to ZModel..."));
|
|
2256
|
+
const deletedModels = [];
|
|
2257
|
+
const deletedEnums = [];
|
|
2258
|
+
const addedModels = [];
|
|
2259
|
+
const addedEnums = [];
|
|
2260
|
+
const modelChanges = /* @__PURE__ */ new Map();
|
|
2261
|
+
const getModelChanges = (modelName) => {
|
|
2262
|
+
if (!modelChanges.has(modelName)) modelChanges.set(modelName, {
|
|
2263
|
+
addedFields: [],
|
|
2264
|
+
deletedFields: [],
|
|
2265
|
+
updatedFields: [],
|
|
2266
|
+
addedAttributes: [],
|
|
2267
|
+
deletedAttributes: [],
|
|
2268
|
+
updatedAttributes: []
|
|
2269
|
+
});
|
|
2270
|
+
return modelChanges.get(modelName);
|
|
2271
|
+
};
|
|
2272
|
+
services.shared.workspace.IndexManager.allElements("DataModel", docsSet).filter((declaration) => !newModel.declarations.find((d) => getDbName(d) === getDbName(declaration.node))).forEach((decl) => {
|
|
2273
|
+
const model = decl.node.$container;
|
|
2274
|
+
const index = model.declarations.findIndex((d) => d === decl.node);
|
|
2275
|
+
model.declarations.splice(index, 1);
|
|
2276
|
+
deletedModels.push(colors.red(`- Model ${decl.name} deleted`));
|
|
2277
|
+
});
|
|
2278
|
+
if (provider.isSupportedFeature("NativeEnum")) services.shared.workspace.IndexManager.allElements("Enum", docsSet).filter((declaration) => !newModel.declarations.find((d) => getDbName(d) === getDbName(declaration.node))).forEach((decl) => {
|
|
2279
|
+
const model = decl.node.$container;
|
|
2280
|
+
const index = model.declarations.findIndex((d) => d === decl.node);
|
|
2281
|
+
model.declarations.splice(index, 1);
|
|
2282
|
+
deletedEnums.push(colors.red(`- Enum ${decl.name} deleted`));
|
|
2283
|
+
});
|
|
2284
|
+
newModel.declarations.filter((d) => [DataModel, Enum].includes(d.$type)).forEach((_declaration) => {
|
|
2285
|
+
const newDataModel = _declaration;
|
|
2286
|
+
const declarations = services.shared.workspace.IndexManager.allElements(newDataModel.$type, docsSet).toArray();
|
|
2287
|
+
const originalDataModel = declarations.find((d) => getDbName(d.node) === getDbName(newDataModel))?.node;
|
|
2288
|
+
if (!originalDataModel) {
|
|
2289
|
+
if (newDataModel.$type === "DataModel") addedModels.push(colors.green(`+ Model ${newDataModel.name} added`));
|
|
2290
|
+
else if (newDataModel.$type === "Enum") addedEnums.push(colors.green(`+ Enum ${newDataModel.name} added`));
|
|
2291
|
+
model.declarations.push(newDataModel);
|
|
2292
|
+
newDataModel.$container = model;
|
|
2293
|
+
newDataModel.fields.forEach((f) => {
|
|
2294
|
+
if (f.$type === "DataField" && f.type.reference?.ref) {
|
|
2295
|
+
const ref = declarations.find((d) => getDbName(d.node) === getDbName(f.type.reference.ref))?.node;
|
|
2296
|
+
if (ref && f.type.reference) f.type.reference = {
|
|
2297
|
+
ref,
|
|
2298
|
+
$refText: ref.name ?? f.type.reference.$refText
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
newDataModel.fields.forEach((f) => {
|
|
2305
|
+
let originalFields = originalDataModel.fields.filter((d) => getDbName(d) === getDbName(f));
|
|
2306
|
+
const isRelationField = f.$type === "DataField" && !!f.attributes?.some((a) => a?.decl?.ref?.name === "@relation");
|
|
2307
|
+
if (originalFields.length === 0 && isRelationField && !getRelationFieldsKey(f)) return;
|
|
2308
|
+
if (originalFields.length === 0) {
|
|
2309
|
+
const newFieldsKey = getRelationFieldsKey(f);
|
|
2310
|
+
if (newFieldsKey) originalFields = originalDataModel.fields.filter((d) => getRelationFieldsKey(d) === newFieldsKey);
|
|
2311
|
+
}
|
|
2312
|
+
if (originalFields.length === 0) originalFields = originalDataModel.fields.filter((d) => getRelationFkName(d) === getRelationFkName(f) && !!getRelationFkName(d) && !!getRelationFkName(f));
|
|
2313
|
+
if (originalFields.length === 0) originalFields = originalDataModel.fields.filter((d) => f.$type === "DataField" && d.$type === "DataField" && f.type.reference?.ref && d.type.reference?.ref && getDbName(f.type.reference.ref) === getDbName(d.type.reference.ref));
|
|
2314
|
+
if (originalFields.length > 1) {
|
|
2315
|
+
if (!!getRelationFieldsKey(f)) console.warn(colors.yellow(`Found more original fields, need to tweak the search algorithm. ${originalDataModel.name}->[${originalFields.map((of) => of.name).join(", ")}](${f.name})`));
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
const originalField = originalFields.at(0);
|
|
2319
|
+
if (originalField && f.$type === "DataField" && originalField.$type === "DataField") {
|
|
2320
|
+
const newType = f.type;
|
|
2321
|
+
const oldType = originalField.type;
|
|
2322
|
+
const fieldUpdates = [];
|
|
2323
|
+
const isOldTypeEnumWithoutNativeSupport = oldType.reference?.ref?.$type === "Enum" && !provider.isSupportedFeature("NativeEnum");
|
|
2324
|
+
if (newType.type && oldType.type !== newType.type && !isOldTypeEnumWithoutNativeSupport) {
|
|
2325
|
+
fieldUpdates.push(`type: ${oldType.type} -> ${newType.type}`);
|
|
2326
|
+
oldType.type = newType.type;
|
|
2327
|
+
}
|
|
2328
|
+
if (newType.reference?.ref && oldType.reference?.ref) {
|
|
2329
|
+
if (getDbName(newType.reference.ref) !== getDbName(oldType.reference.ref)) {
|
|
2330
|
+
fieldUpdates.push(`reference: ${oldType.reference.$refText} -> ${newType.reference.$refText}`);
|
|
2331
|
+
oldType.reference = {
|
|
2332
|
+
ref: newType.reference.ref,
|
|
2333
|
+
$refText: newType.reference.$refText
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
} else if (newType.reference?.ref && !oldType.reference) {
|
|
2337
|
+
fieldUpdates.push(`type: ${oldType.type} -> ${newType.reference.$refText}`);
|
|
2338
|
+
oldType.reference = newType.reference;
|
|
2339
|
+
oldType.type = void 0;
|
|
2340
|
+
} else if (!newType.reference && oldType.reference?.ref && newType.type) {
|
|
2341
|
+
if (!(oldType.reference.ref.$type === "Enum" && !provider.isSupportedFeature("NativeEnum"))) {
|
|
2342
|
+
fieldUpdates.push(`type: ${oldType.reference.$refText} -> ${newType.type}`);
|
|
2343
|
+
oldType.type = newType.type;
|
|
2344
|
+
oldType.reference = void 0;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
if (!!newType.optional !== !!oldType.optional) {
|
|
2348
|
+
fieldUpdates.push(`optional: ${!!oldType.optional} -> ${!!newType.optional}`);
|
|
2349
|
+
oldType.optional = newType.optional;
|
|
2350
|
+
}
|
|
2351
|
+
if (!!newType.array !== !!oldType.array) {
|
|
2352
|
+
fieldUpdates.push(`array: ${!!oldType.array} -> ${!!newType.array}`);
|
|
2353
|
+
oldType.array = newType.array;
|
|
2354
|
+
}
|
|
2355
|
+
if (fieldUpdates.length > 0) getModelChanges(originalDataModel.name).updatedFields.push(colors.yellow(`~ ${originalField.name} (${fieldUpdates.join(", ")})`));
|
|
2356
|
+
const newDefaultAttr = f.attributes.find((a) => a.decl.$refText === "@default");
|
|
2357
|
+
const oldDefaultAttr = originalField.attributes.find((a) => a.decl.$refText === "@default");
|
|
2358
|
+
if (newDefaultAttr && oldDefaultAttr) {
|
|
2359
|
+
const serializeArgs = (args) => args.map((arg) => {
|
|
2360
|
+
if (arg.value?.$type === "StringLiteral") return `"${arg.value.value}"`;
|
|
2361
|
+
if (arg.value?.$type === "NumberLiteral") return String(arg.value.value);
|
|
2362
|
+
if (arg.value?.$type === "BooleanLiteral") return String(arg.value.value);
|
|
2363
|
+
if (arg.value?.$type === "InvocationExpr") return arg.value.function?.$refText ?? "";
|
|
2364
|
+
if (arg.value?.$type === "ReferenceExpr") return arg.value.target?.$refText ?? "";
|
|
2365
|
+
if (arg.value?.$type === "ArrayExpr") return `[${(arg.value.items ?? []).map((item) => {
|
|
2366
|
+
if (item.$type === "ReferenceExpr") return item.target?.$refText ?? "";
|
|
2367
|
+
return item.$type ?? "unknown";
|
|
2368
|
+
}).join(",")}]`;
|
|
2369
|
+
return arg.value?.$type ?? "unknown";
|
|
2370
|
+
}).join(",");
|
|
2371
|
+
if (serializeArgs(newDefaultAttr.args ?? []) !== serializeArgs(oldDefaultAttr.args ?? [])) {
|
|
2372
|
+
oldDefaultAttr.args = newDefaultAttr.args.map((arg) => ({
|
|
2373
|
+
...arg,
|
|
2374
|
+
$container: oldDefaultAttr
|
|
2375
|
+
}));
|
|
2376
|
+
getModelChanges(originalDataModel.name).updatedAttributes.push(colors.yellow(`~ @default on ${originalDataModel.name}.${originalField.name}`));
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
if (!originalField) {
|
|
2381
|
+
getModelChanges(originalDataModel.name).addedFields.push(colors.green(`+ ${f.name}`));
|
|
2382
|
+
f.$container = originalDataModel;
|
|
2383
|
+
originalDataModel.fields.push(f);
|
|
2384
|
+
if (f.$type === "DataField" && f.type.reference?.ref) {
|
|
2385
|
+
const ref = declarations.find((d) => getDbName(d.node) === getDbName(f.type.reference.ref))?.node;
|
|
2386
|
+
if (ref) f.type.reference = {
|
|
2387
|
+
ref,
|
|
2388
|
+
$refText: ref.name ?? f.type.reference.$refText
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
originalField.attributes.filter((attr) => !f.attributes.find((d) => d.decl.$refText === attr.decl.$refText) && isDatabaseManagedAttribute(attr.decl.$refText)).forEach((attr) => {
|
|
2394
|
+
const field = attr.$container;
|
|
2395
|
+
const index = field.attributes.findIndex((d) => d === attr);
|
|
2396
|
+
field.attributes.splice(index, 1);
|
|
2397
|
+
getModelChanges(originalDataModel.name).deletedAttributes.push(colors.yellow(`- ${attr.decl.$refText} from field: ${originalDataModel.name}.${field.name}`));
|
|
2398
|
+
});
|
|
2399
|
+
f.attributes.filter((attr) => !originalField.attributes.find((d) => d.decl.$refText === attr.decl.$refText) && isDatabaseManagedAttribute(attr.decl.$refText)).forEach((attr) => {
|
|
2400
|
+
const cloned = {
|
|
2401
|
+
...attr,
|
|
2402
|
+
$container: originalField
|
|
2403
|
+
};
|
|
2404
|
+
originalField.attributes.push(cloned);
|
|
2405
|
+
getModelChanges(originalDataModel.name).addedAttributes.push(colors.green(`+ ${attr.decl.$refText} to field: ${originalDataModel.name}.${f.name}`));
|
|
2406
|
+
});
|
|
2407
|
+
});
|
|
2408
|
+
originalDataModel.fields.filter((f) => {
|
|
2409
|
+
if (newDataModel.fields.find((d) => getDbName(d) === getDbName(f))) return false;
|
|
2410
|
+
const originalFieldsKey = getRelationFieldsKey(f);
|
|
2411
|
+
if (originalFieldsKey) {
|
|
2412
|
+
if (newDataModel.fields.find((d) => getRelationFieldsKey(d) === originalFieldsKey)) return false;
|
|
2413
|
+
}
|
|
2414
|
+
if (newDataModel.fields.find((d) => getRelationFkName(d) === getRelationFkName(f) && !!getRelationFkName(d) && !!getRelationFkName(f))) return false;
|
|
2415
|
+
return !newDataModel.fields.find((d) => f.$type === "DataField" && d.$type === "DataField" && f.type.reference?.ref && d.type.reference?.ref && getDbName(f.type.reference.ref) === getDbName(d.type.reference.ref));
|
|
2416
|
+
}).forEach((f) => {
|
|
2417
|
+
const _model = f.$container;
|
|
2418
|
+
const index = _model.fields.findIndex((d) => d === f);
|
|
2419
|
+
_model.fields.splice(index, 1);
|
|
2420
|
+
getModelChanges(_model.name).deletedFields.push(colors.red(`- ${f.name}`));
|
|
2421
|
+
});
|
|
2422
|
+
});
|
|
2423
|
+
if (deletedModels.length > 0) {
|
|
2424
|
+
console.log(colors.bold("\nDeleted Models:"));
|
|
2425
|
+
deletedModels.forEach((msg) => {
|
|
2426
|
+
console.log(msg);
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
if (deletedEnums.length > 0) {
|
|
2430
|
+
console.log(colors.bold("\nDeleted Enums:"));
|
|
2431
|
+
deletedEnums.forEach((msg) => {
|
|
2432
|
+
console.log(msg);
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
if (addedModels.length > 0) {
|
|
2436
|
+
console.log(colors.bold("\nAdded Models:"));
|
|
2437
|
+
addedModels.forEach((msg) => {
|
|
2438
|
+
console.log(msg);
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
if (addedEnums.length > 0) {
|
|
2442
|
+
console.log(colors.bold("\nAdded Enums:"));
|
|
2443
|
+
addedEnums.forEach((msg) => {
|
|
2444
|
+
console.log(msg);
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
if (modelChanges.size > 0) {
|
|
2448
|
+
console.log(colors.bold("\nModel Changes:"));
|
|
2449
|
+
modelChanges.forEach((changes, modelName) => {
|
|
2450
|
+
if (changes.addedFields.length > 0 || changes.deletedFields.length > 0 || changes.updatedFields.length > 0 || changes.addedAttributes.length > 0 || changes.deletedAttributes.length > 0 || changes.updatedAttributes.length > 0) {
|
|
2451
|
+
console.log(colors.cyan(` ${modelName}:`));
|
|
2452
|
+
if (changes.addedFields.length > 0) {
|
|
2453
|
+
console.log(colors.gray(" Added Fields:"));
|
|
2454
|
+
changes.addedFields.forEach((msg) => {
|
|
2455
|
+
console.log(` ${msg}`);
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
if (changes.deletedFields.length > 0) {
|
|
2459
|
+
console.log(colors.gray(" Deleted Fields:"));
|
|
2460
|
+
changes.deletedFields.forEach((msg) => {
|
|
2461
|
+
console.log(` ${msg}`);
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
if (changes.updatedFields.length > 0) {
|
|
2465
|
+
console.log(colors.gray(" Updated Fields:"));
|
|
2466
|
+
changes.updatedFields.forEach((msg) => {
|
|
2467
|
+
console.log(` ${msg}`);
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
if (changes.addedAttributes.length > 0) {
|
|
2471
|
+
console.log(colors.gray(" Added Attributes:"));
|
|
2472
|
+
changes.addedAttributes.forEach((msg) => {
|
|
2473
|
+
console.log(` ${msg}`);
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
if (changes.deletedAttributes.length > 0) {
|
|
2477
|
+
console.log(colors.gray(" Deleted Attributes:"));
|
|
2478
|
+
changes.deletedAttributes.forEach((msg) => {
|
|
2479
|
+
console.log(` ${msg}`);
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
if (changes.updatedAttributes.length > 0) {
|
|
2483
|
+
console.log(colors.gray(" Updated Attributes:"));
|
|
2484
|
+
changes.updatedAttributes.forEach((msg) => {
|
|
2485
|
+
console.log(` ${msg}`);
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
const generator = new ZModelCodeGenerator({
|
|
2492
|
+
quote: options.quote ?? "single",
|
|
2493
|
+
indent: options.indent ?? 4
|
|
2494
|
+
});
|
|
2495
|
+
if (options.output) if (treatAsFile) {
|
|
2496
|
+
const zmodelSchema = await formatDocument(generator.generate(newModel));
|
|
2497
|
+
console.log(colors.blue(`Writing to ${outPath}`));
|
|
2498
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
2499
|
+
fs.writeFileSync(outPath, zmodelSchema);
|
|
2500
|
+
} else {
|
|
2501
|
+
fs.mkdirSync(outPath, { recursive: true });
|
|
2502
|
+
const baseDir = path.dirname(path.resolve(schemaFile));
|
|
2503
|
+
for (const { uri, parseResult: { value: documentModel } } of docs) {
|
|
2504
|
+
const zmodelSchema = await formatDocument(generator.generate(documentModel));
|
|
2505
|
+
const relPath = path.relative(baseDir, uri.fsPath);
|
|
2506
|
+
const targetFile = path.join(outPath, relPath);
|
|
2507
|
+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
2508
|
+
console.log(colors.blue(`Writing to ${targetFile}`));
|
|
2509
|
+
fs.writeFileSync(targetFile, zmodelSchema);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
else for (const { uri, parseResult: { value: documentModel } } of docs) {
|
|
2513
|
+
const zmodelSchema = await formatDocument(generator.generate(documentModel));
|
|
2514
|
+
console.log(colors.blue(`Writing to ${path.relative(process.cwd(), uri.fsPath).replace(/\\/g, "/")}`));
|
|
2515
|
+
fs.writeFileSync(uri.fsPath, zmodelSchema);
|
|
2516
|
+
}
|
|
2517
|
+
console.log(colors.green.bold("\nPull completed successfully!"));
|
|
2518
|
+
} catch (error) {
|
|
2519
|
+
spinner.fail("Pull failed");
|
|
2520
|
+
console.error(error);
|
|
2521
|
+
throw error;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
//#endregion
|
|
2525
|
+
//#region src/actions/format.ts
|
|
2526
|
+
/**
|
|
2527
|
+
* CLI action for formatting a ZModel schema file.
|
|
2528
|
+
*/
|
|
2529
|
+
async function run$6(options) {
|
|
2530
|
+
const schemaFile = getSchemaFile(options.schema);
|
|
2531
|
+
let formattedContent;
|
|
2532
|
+
try {
|
|
2533
|
+
formattedContent = await formatDocument(fs.readFileSync(schemaFile, "utf-8"));
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
console.error(colors.red("✗ Schema formatting failed."));
|
|
2536
|
+
throw error;
|
|
2537
|
+
}
|
|
2538
|
+
fs.writeFileSync(schemaFile, formattedContent, "utf-8");
|
|
2539
|
+
console.log(colors.green("✓ Schema formatting completed successfully."));
|
|
2540
|
+
}
|
|
2541
|
+
//#endregion
|
|
2542
|
+
//#region src/plugins/prisma.ts
|
|
2543
|
+
const plugin$1 = {
|
|
2544
|
+
name: "Prisma Schema Generator",
|
|
2545
|
+
statusText: "Generating Prisma schema",
|
|
2546
|
+
async generate({ model, defaultOutputPath, pluginOptions }) {
|
|
2547
|
+
let outFile = path.join(defaultOutputPath, "schema.prisma");
|
|
2548
|
+
if (typeof pluginOptions["output"] === "string") {
|
|
2549
|
+
outFile = path.resolve(defaultOutputPath, pluginOptions["output"]);
|
|
2550
|
+
if (!fs.existsSync(path.dirname(outFile))) fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
2551
|
+
}
|
|
2552
|
+
const prismaSchema = await new PrismaSchemaGenerator(model).generate();
|
|
2553
|
+
fs.writeFileSync(outFile, prismaSchema);
|
|
2554
|
+
}
|
|
2555
|
+
};
|
|
2556
|
+
//#endregion
|
|
2557
|
+
//#region src/plugins/typescript.ts
|
|
2558
|
+
const plugin = {
|
|
2559
|
+
name: "TypeScript Schema Generator",
|
|
2560
|
+
statusText: "Generating TypeScript schema",
|
|
2561
|
+
async generate({ model, defaultOutputPath, pluginOptions }) {
|
|
2562
|
+
let outDir = defaultOutputPath;
|
|
2563
|
+
if (typeof pluginOptions["output"] === "string") {
|
|
2564
|
+
outDir = path.resolve(defaultOutputPath, pluginOptions["output"]);
|
|
2565
|
+
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
2566
|
+
}
|
|
2567
|
+
const lite = pluginOptions["lite"] === true;
|
|
2568
|
+
const liteOnly = pluginOptions["liteOnly"] === true;
|
|
2569
|
+
const importWithFileExtension = pluginOptions["importWithFileExtension"];
|
|
2570
|
+
if (importWithFileExtension && typeof importWithFileExtension !== "string") throw new Error("The \"importWithFileExtension\" option must be a string if specified.");
|
|
2571
|
+
const generateModelTypes = pluginOptions["generateModels"] !== false;
|
|
2572
|
+
const generateInputTypes = pluginOptions["generateInput"] !== false;
|
|
2573
|
+
await new TsSchemaGenerator().generate(model, {
|
|
2574
|
+
outDir,
|
|
2575
|
+
lite,
|
|
2576
|
+
liteOnly,
|
|
2577
|
+
importWithFileExtension,
|
|
2578
|
+
generateModelTypes,
|
|
2579
|
+
generateInputTypes
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
//#endregion
|
|
2584
|
+
//#region src/plugins/index.ts
|
|
2585
|
+
var plugins_exports = /* @__PURE__ */ __exportAll({
|
|
2586
|
+
prisma: () => plugin$1,
|
|
2587
|
+
typescript: () => plugin
|
|
2588
|
+
});
|
|
2589
|
+
//#endregion
|
|
2590
|
+
//#region src/actions/generate.ts
|
|
2591
|
+
/**
|
|
2592
|
+
* CLI action for generating code from schema
|
|
2593
|
+
*/
|
|
2594
|
+
async function run$5(options) {
|
|
2595
|
+
try {
|
|
2596
|
+
await checkForMismatchedPackages(process.cwd());
|
|
2597
|
+
} catch (err) {
|
|
2598
|
+
console.warn(colors.yellow(`Failed to check for mismatched ZenStack packages: ${err}`));
|
|
2599
|
+
}
|
|
2600
|
+
const maybeShowUsageTips = options.tips && !options.silent && !options.watch ? startUsageTipsFetch() : void 0;
|
|
2601
|
+
const model = await pureGenerate(options, false);
|
|
2602
|
+
await maybeShowUsageTips?.();
|
|
2603
|
+
if (options.watch) {
|
|
2604
|
+
const logsEnabled = !options.silent;
|
|
2605
|
+
if (logsEnabled) console.log(colors.green(`\nEnabled watch mode!`));
|
|
2606
|
+
const schemaExtensions = ZModelLanguageMetaData.fileExtensions;
|
|
2607
|
+
const getRootModelWatchPaths = (model) => new Set(model.declarations.filter((v) => v.$cstNode?.parent?.element.$type === "Model" && !!v.$cstNode.parent.element.$document?.uri?.fsPath).map((v) => v.$cstNode.parent.element.$document.uri.fsPath));
|
|
2608
|
+
const watchedPaths = getRootModelWatchPaths(model);
|
|
2609
|
+
if (logsEnabled) {
|
|
2610
|
+
const logPaths = [...watchedPaths].map((at) => `- ${at}`).join("\n");
|
|
2611
|
+
console.log(`Watched file paths:\n${logPaths}`);
|
|
2612
|
+
}
|
|
2613
|
+
const watcher = watch([...watchedPaths], {
|
|
2614
|
+
alwaysStat: false,
|
|
2615
|
+
ignoreInitial: true,
|
|
2616
|
+
ignorePermissionErrors: true,
|
|
2617
|
+
ignored: (at) => !schemaExtensions.some((ext) => at.endsWith(ext))
|
|
2618
|
+
});
|
|
2619
|
+
const reGenerateSchema = singleDebounce(async () => {
|
|
2620
|
+
if (logsEnabled) console.log("Got changes, run generation!");
|
|
2621
|
+
try {
|
|
2622
|
+
const allModelsPaths = getRootModelWatchPaths(await pureGenerate(options, true));
|
|
2623
|
+
const newModelPaths = [...allModelsPaths].filter((at) => !watchedPaths.has(at));
|
|
2624
|
+
const removeModelPaths = [...watchedPaths].filter((at) => !allModelsPaths.has(at));
|
|
2625
|
+
if (newModelPaths.length) {
|
|
2626
|
+
if (logsEnabled) {
|
|
2627
|
+
const logPaths = newModelPaths.map((at) => `- ${at}`).join("\n");
|
|
2628
|
+
console.log(`Added file(s) to watch:\n${logPaths}`);
|
|
2629
|
+
}
|
|
2630
|
+
newModelPaths.forEach((at) => watchedPaths.add(at));
|
|
2631
|
+
watcher.add(newModelPaths);
|
|
2632
|
+
}
|
|
2633
|
+
if (removeModelPaths.length) {
|
|
2634
|
+
if (logsEnabled) {
|
|
2635
|
+
const logPaths = removeModelPaths.map((at) => `- ${at}`).join("\n");
|
|
2636
|
+
console.log(`Removed file(s) from watch:\n${logPaths}`);
|
|
2637
|
+
}
|
|
2638
|
+
removeModelPaths.forEach((at) => watchedPaths.delete(at));
|
|
2639
|
+
watcher.unwatch(removeModelPaths);
|
|
2640
|
+
}
|
|
2641
|
+
} catch (e) {
|
|
2642
|
+
console.error(e);
|
|
2643
|
+
}
|
|
2644
|
+
}, 500, true);
|
|
2645
|
+
watcher.on("unlink", (pathAt) => {
|
|
2646
|
+
if (logsEnabled) console.log(`Removed file from watch: ${pathAt}`);
|
|
2647
|
+
watchedPaths.delete(pathAt);
|
|
2648
|
+
watcher.unwatch(pathAt);
|
|
2649
|
+
reGenerateSchema();
|
|
2650
|
+
});
|
|
2651
|
+
watcher.on("change", () => {
|
|
2652
|
+
reGenerateSchema();
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
async function pureGenerate(options, fromWatch) {
|
|
2657
|
+
const start = Date.now();
|
|
2658
|
+
const schemaFile = getSchemaFile(options.schema);
|
|
2659
|
+
const model = await loadSchemaDocument(schemaFile);
|
|
2660
|
+
const outputPath = getOutputPath(options, schemaFile);
|
|
2661
|
+
await runPlugins(schemaFile, model, outputPath, options);
|
|
2662
|
+
if (!options.silent) {
|
|
2663
|
+
console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`));
|
|
2664
|
+
if (!fromWatch) console.log(`You can now create a ZenStack client with it.
|
|
2665
|
+
|
|
2666
|
+
\`\`\`ts
|
|
2667
|
+
import { ZenStackClient } from '@zenstackhq/orm';
|
|
2668
|
+
import { schema } from '${path.relative(".", outputPath)}/schema';
|
|
2669
|
+
|
|
2670
|
+
const client = new ZenStackClient(schema, {
|
|
2671
|
+
dialect: { ... }
|
|
2672
|
+
});
|
|
2673
|
+
\`\`\`
|
|
2674
|
+
|
|
2675
|
+
Check documentation: https://zenstack.dev/docs/`);
|
|
2676
|
+
}
|
|
2677
|
+
return model;
|
|
2678
|
+
}
|
|
2679
|
+
async function runPlugins(schemaFile, model, outputPath, options) {
|
|
2680
|
+
const plugins = model.declarations.filter(isPlugin);
|
|
2681
|
+
const processedPlugins = [];
|
|
2682
|
+
for (const plugin of plugins) {
|
|
2683
|
+
const provider = getPluginProvider(plugin);
|
|
2684
|
+
let cliPlugin;
|
|
2685
|
+
if (provider.startsWith("@core/")) {
|
|
2686
|
+
cliPlugin = plugins_exports[provider.slice(6)];
|
|
2687
|
+
if (!cliPlugin) throw new CliError(`Unknown core plugin: ${provider}`);
|
|
2688
|
+
} else {
|
|
2689
|
+
const pluginSourcePath = plugin.$cstNode?.parent?.element.$document?.uri?.fsPath ?? schemaFile;
|
|
2690
|
+
cliPlugin = await loadPluginModule(provider, path.dirname(pluginSourcePath));
|
|
2691
|
+
}
|
|
2692
|
+
if (cliPlugin) {
|
|
2693
|
+
const pluginOptions = getPluginOptions(plugin);
|
|
2694
|
+
if (provider === "@core/typescript") {
|
|
2695
|
+
if (options.lite !== void 0) pluginOptions["lite"] = options.lite;
|
|
2696
|
+
if (options.liteOnly !== void 0) pluginOptions["liteOnly"] = options.liteOnly;
|
|
2697
|
+
if (options.generateModels !== void 0) pluginOptions["generateModels"] = options.generateModels;
|
|
2698
|
+
if (options.generateInput !== void 0) pluginOptions["generateInput"] = options.generateInput;
|
|
2699
|
+
}
|
|
2700
|
+
processedPlugins.push({
|
|
2701
|
+
cliPlugin,
|
|
2702
|
+
pluginOptions
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
[{
|
|
2707
|
+
plugin,
|
|
2708
|
+
options: {
|
|
2709
|
+
lite: options.lite,
|
|
2710
|
+
liteOnly: options.liteOnly,
|
|
2711
|
+
generateModels: options.generateModels,
|
|
2712
|
+
generateInput: options.generateInput
|
|
2713
|
+
}
|
|
2714
|
+
}].forEach(({ plugin, options }) => {
|
|
2715
|
+
if (!processedPlugins.some((p) => p.cliPlugin === plugin)) processedPlugins.unshift({
|
|
2716
|
+
cliPlugin: plugin,
|
|
2717
|
+
pluginOptions: options
|
|
2718
|
+
});
|
|
2719
|
+
});
|
|
2720
|
+
for (const { cliPlugin, pluginOptions } of processedPlugins) {
|
|
2721
|
+
invariant(typeof cliPlugin.generate === "function", `Plugin ${cliPlugin.name} does not have a generate function`);
|
|
2722
|
+
let spinner;
|
|
2723
|
+
if (!options.silent) spinner = ora(cliPlugin.statusText ?? `Running plugin ${cliPlugin.name}`).start();
|
|
2724
|
+
try {
|
|
2725
|
+
await cliPlugin.generate({
|
|
2726
|
+
schemaFile,
|
|
2727
|
+
model,
|
|
2728
|
+
defaultOutputPath: outputPath,
|
|
2729
|
+
pluginOptions
|
|
2730
|
+
});
|
|
2731
|
+
spinner?.succeed();
|
|
2732
|
+
} catch (err) {
|
|
2733
|
+
spinner?.fail();
|
|
2734
|
+
throw err;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
function getPluginOptions(plugin) {
|
|
2739
|
+
const result = {};
|
|
2740
|
+
for (const field of plugin.fields) {
|
|
2741
|
+
if (field.name === "provider") continue;
|
|
2742
|
+
const value = getLiteral(field.value) ?? getLiteralArray(field.value);
|
|
2743
|
+
if (value === void 0) {
|
|
2744
|
+
console.warn(`Plugin "${plugin.name}" option "${field.name}" has unsupported value, skipping`);
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
result[field.name] = value;
|
|
2748
|
+
}
|
|
2749
|
+
return result;
|
|
2750
|
+
}
|
|
2751
|
+
async function checkForMismatchedPackages(projectPath) {
|
|
2752
|
+
const packages = await getZenStackPackages(projectPath);
|
|
2753
|
+
if (!packages.length) return false;
|
|
2754
|
+
const versions = /* @__PURE__ */ new Set();
|
|
2755
|
+
for (const { version } of packages) if (version) versions.add(version);
|
|
2756
|
+
if (versions.size > 1) {
|
|
2757
|
+
const message = "WARNING: Multiple versions of ZenStack packages detected.\n This will probably cause issues and break your types.";
|
|
2758
|
+
const slashes = "/".repeat(73);
|
|
2759
|
+
const latestVersion = semver.sort(Array.from(versions)).reverse()[0];
|
|
2760
|
+
console.warn(colors.yellow(`${slashes}\n\n\t${message}\n`));
|
|
2761
|
+
for (const { pkg, version } of packages) {
|
|
2762
|
+
if (!version) continue;
|
|
2763
|
+
if (version === latestVersion) console.log(`\t${pkg.padEnd(32)}\t${colors.green(version)}`);
|
|
2764
|
+
else console.log(`\t${pkg.padEnd(32)}\t${colors.yellow(version)}`);
|
|
2765
|
+
}
|
|
2766
|
+
console.warn(`\n${colors.yellow(slashes)}`);
|
|
2767
|
+
return true;
|
|
2768
|
+
}
|
|
2769
|
+
return false;
|
|
2770
|
+
}
|
|
2771
|
+
//#endregion
|
|
2772
|
+
//#region src/actions/info.ts
|
|
2773
|
+
/**
|
|
2774
|
+
* CLI action for getting information about installed ZenStack packages
|
|
2775
|
+
*/
|
|
2776
|
+
async function run$4(projectPath) {
|
|
2777
|
+
const packages = await getZenStackPackages(projectPath);
|
|
2778
|
+
if (!packages.length) {
|
|
2779
|
+
console.error("Unable to locate package.json. Are you in a valid project directory?");
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
console.log("Installed ZenStack Packages:");
|
|
2783
|
+
const versions = /* @__PURE__ */ new Set();
|
|
2784
|
+
for (const { pkg, version } of packages) {
|
|
2785
|
+
if (version) versions.add(version);
|
|
2786
|
+
console.log(` ${colors.green(pkg.padEnd(20))}\t${version}`);
|
|
2787
|
+
}
|
|
2788
|
+
if (versions.size > 1) console.warn(colors.yellow("WARNING: Multiple versions of Zenstack packages detected. This may cause issues."));
|
|
2789
|
+
}
|
|
2790
|
+
//#endregion
|
|
2791
|
+
//#region src/actions/templates.ts
|
|
2792
|
+
const STARTER_ZMODEL = `// This is a sample model to get you started.
|
|
2793
|
+
|
|
2794
|
+
/// A sample data source using local sqlite db.
|
|
2795
|
+
datasource db {
|
|
2796
|
+
provider = 'sqlite'
|
|
2797
|
+
url = 'file:./dev.db'
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
/// User model
|
|
2801
|
+
model User {
|
|
2802
|
+
id String @id @default(cuid())
|
|
2803
|
+
email String @unique @email @length(6, 32)
|
|
2804
|
+
posts Post[]
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
/// Post model
|
|
2808
|
+
model Post {
|
|
2809
|
+
id String @id @default(cuid())
|
|
2810
|
+
createdAt DateTime @default(now())
|
|
2811
|
+
updatedAt DateTime @updatedAt
|
|
2812
|
+
title String @length(1, 256)
|
|
2813
|
+
content String
|
|
2814
|
+
published Boolean @default(false)
|
|
2815
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
2816
|
+
authorId String
|
|
2817
|
+
}
|
|
2818
|
+
`;
|
|
2819
|
+
//#endregion
|
|
2820
|
+
//#region src/actions/init.ts
|
|
2821
|
+
/**
|
|
2822
|
+
* CLI action for getting information about installed ZenStack packages
|
|
2823
|
+
*/
|
|
2824
|
+
async function run$3(projectPath) {
|
|
2825
|
+
const packages = [
|
|
2826
|
+
{
|
|
2827
|
+
name: "@zenstackhq/cli@latest",
|
|
2828
|
+
dev: true
|
|
2829
|
+
},
|
|
2830
|
+
{
|
|
2831
|
+
name: "@zenstackhq/schema@latest",
|
|
2832
|
+
dev: false
|
|
2833
|
+
},
|
|
2834
|
+
{
|
|
2835
|
+
name: "@zenstackhq/orm@latest",
|
|
2836
|
+
dev: false
|
|
2837
|
+
}
|
|
2838
|
+
];
|
|
2839
|
+
let pm = await detect();
|
|
2840
|
+
if (!pm) pm = {
|
|
2841
|
+
agent: "npm",
|
|
2842
|
+
name: "npm"
|
|
2843
|
+
};
|
|
2844
|
+
console.log(colors.gray(`Using package manager: ${pm.agent}`));
|
|
2845
|
+
for (const pkg of packages) {
|
|
2846
|
+
const resolved = resolveCommand(pm.agent, "add", [pkg.name, ...pkg.dev ? [pm.agent.startsWith("yarn") || pm.agent === "bun" ? "--dev" : "--save-dev"] : []]);
|
|
2847
|
+
if (!resolved) throw new CliError(`Unable to determine how to install package "${pkg.name}". Please install it manually.`);
|
|
2848
|
+
const spinner = ora(`Installing "${pkg.name}"`).start();
|
|
2849
|
+
try {
|
|
2850
|
+
execSync$1(`${resolved.command} ${resolved.args.join(" ")}`, { cwd: projectPath });
|
|
2851
|
+
spinner.succeed();
|
|
2852
|
+
} catch (e) {
|
|
2853
|
+
spinner.fail();
|
|
2854
|
+
throw e;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
const generationFolder = "zenstack";
|
|
2858
|
+
if (!fs.existsSync(path.join(projectPath, generationFolder))) fs.mkdirSync(path.join(projectPath, generationFolder));
|
|
2859
|
+
if (!fs.existsSync(path.join(projectPath, generationFolder, "schema.zmodel"))) fs.writeFileSync(path.join(projectPath, generationFolder, "schema.zmodel"), STARTER_ZMODEL);
|
|
2860
|
+
else console.log(colors.yellow("Schema file already exists. Skipping generation of sample."));
|
|
2861
|
+
console.log(colors.green("ZenStack project initialized successfully!"));
|
|
2862
|
+
console.log(colors.gray(`See "${generationFolder}/schema.zmodel" for your database schema.`));
|
|
2863
|
+
console.log(colors.gray("Run `zenstack generate` to compile the the schema into a TypeScript file."));
|
|
2864
|
+
}
|
|
2865
|
+
//#endregion
|
|
2866
|
+
//#region src/actions/seed.ts
|
|
2867
|
+
/**
|
|
2868
|
+
* CLI action for seeding the database.
|
|
2869
|
+
*/
|
|
2870
|
+
async function run$2(options, args) {
|
|
2871
|
+
const pkgJsonConfig = getPkgJsonConfig(process.cwd());
|
|
2872
|
+
if (!pkgJsonConfig.seed) {
|
|
2873
|
+
if (!options.noWarnings) console.warn(colors.yellow("No seed script defined in package.json. Skipping seeding."));
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
const command = `${pkgJsonConfig.seed}${args.length > 0 ? " " + args.join(" ") : ""}`;
|
|
2877
|
+
if (options.printStatus) console.log(colors.gray(`Running seed script "${command}"...`));
|
|
2878
|
+
try {
|
|
2879
|
+
await execaCommand(command, {
|
|
2880
|
+
stdout: "inherit",
|
|
2881
|
+
stderr: "inherit"
|
|
2882
|
+
});
|
|
2883
|
+
} catch (err) {
|
|
2884
|
+
console.error(colors.red(err instanceof Error ? err.message : String(err)));
|
|
2885
|
+
throw new CliError("Failed to seed the database. Please check the error message above for details.");
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
//#endregion
|
|
2889
|
+
//#region src/actions/migrate.ts
|
|
2890
|
+
/**
|
|
2891
|
+
* CLI action for migration-related commands
|
|
2892
|
+
*/
|
|
2893
|
+
async function run$1(command, options) {
|
|
2894
|
+
const schemaFile = getSchemaFile(options.schema);
|
|
2895
|
+
await requireDataSourceUrl(schemaFile);
|
|
2896
|
+
const prismaSchemaFile = await generateTempPrismaSchema(schemaFile, options.migrations ? path.dirname(options.migrations) : void 0);
|
|
2897
|
+
try {
|
|
2898
|
+
switch (command) {
|
|
2899
|
+
case "dev":
|
|
2900
|
+
await runDev(prismaSchemaFile, options);
|
|
2901
|
+
break;
|
|
2902
|
+
case "reset":
|
|
2903
|
+
await runReset(prismaSchemaFile, options);
|
|
2904
|
+
break;
|
|
2905
|
+
case "deploy":
|
|
2906
|
+
await runDeploy(prismaSchemaFile, options);
|
|
2907
|
+
break;
|
|
2908
|
+
case "status":
|
|
2909
|
+
await runStatus(prismaSchemaFile, options);
|
|
2910
|
+
break;
|
|
2911
|
+
case "resolve":
|
|
2912
|
+
await runResolve(prismaSchemaFile, options);
|
|
2913
|
+
break;
|
|
2914
|
+
}
|
|
2915
|
+
} finally {
|
|
2916
|
+
if (fs.existsSync(prismaSchemaFile)) fs.unlinkSync(prismaSchemaFile);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
function runDev(prismaSchemaFile, options) {
|
|
2920
|
+
try {
|
|
2921
|
+
execPrisma([
|
|
2922
|
+
"migrate dev",
|
|
2923
|
+
` --schema "${prismaSchemaFile}"`,
|
|
2924
|
+
" --skip-generate",
|
|
2925
|
+
" --skip-seed",
|
|
2926
|
+
options.name ? ` --name "${options.name}"` : "",
|
|
2927
|
+
options.createOnly ? " --create-only" : ""
|
|
2928
|
+
].join(""));
|
|
2929
|
+
} catch (err) {
|
|
2930
|
+
handleSubProcessError(err);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
async function runReset(prismaSchemaFile, options) {
|
|
2934
|
+
try {
|
|
2935
|
+
execPrisma([
|
|
2936
|
+
"migrate reset",
|
|
2937
|
+
` --schema "${prismaSchemaFile}"`,
|
|
2938
|
+
" --skip-generate",
|
|
2939
|
+
" --skip-seed",
|
|
2940
|
+
options.force ? " --force" : ""
|
|
2941
|
+
].join(""));
|
|
2942
|
+
} catch (err) {
|
|
2943
|
+
handleSubProcessError(err);
|
|
2944
|
+
}
|
|
2945
|
+
if (!options.skipSeed) await run$2({
|
|
2946
|
+
noWarnings: true,
|
|
2947
|
+
printStatus: true
|
|
2948
|
+
}, []);
|
|
2949
|
+
}
|
|
2950
|
+
function runDeploy(prismaSchemaFile, _options) {
|
|
2951
|
+
try {
|
|
2952
|
+
execPrisma(["migrate deploy", ` --schema "${prismaSchemaFile}"`].join(""));
|
|
2953
|
+
} catch (err) {
|
|
2954
|
+
handleSubProcessError(err);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
function runStatus(prismaSchemaFile, _options) {
|
|
2958
|
+
try {
|
|
2959
|
+
execPrisma(`migrate status --schema "${prismaSchemaFile}"`);
|
|
2960
|
+
} catch (err) {
|
|
2961
|
+
handleSubProcessError(err);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
function runResolve(prismaSchemaFile, options) {
|
|
2965
|
+
if (!options.applied && !options.rolledBack) throw new CliError("Either --applied or --rolled-back option must be provided");
|
|
2966
|
+
try {
|
|
2967
|
+
execPrisma([
|
|
2968
|
+
"migrate resolve",
|
|
2969
|
+
` --schema "${prismaSchemaFile}"`,
|
|
2970
|
+
options.applied ? ` --applied "${options.applied}"` : "",
|
|
2971
|
+
options.rolledBack ? ` --rolled-back "${options.rolledBack}"` : ""
|
|
2972
|
+
].join(""));
|
|
2973
|
+
} catch (err) {
|
|
2974
|
+
handleSubProcessError(err);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
function handleSubProcessError(err) {
|
|
2978
|
+
if (err instanceof Error && "status" in err && typeof err.status === "number") process.exit(err.status);
|
|
2979
|
+
else process.exit(1);
|
|
2980
|
+
}
|
|
2981
|
+
//#endregion
|
|
2982
|
+
//#region src/utils/version-utils.ts
|
|
2983
|
+
const CHECK_VERSION_TIMEOUT = 2e3;
|
|
2984
|
+
const VERSION_CHECK_TAG = "latest";
|
|
2985
|
+
function getVersion() {
|
|
2986
|
+
try {
|
|
2987
|
+
const _dirname = typeof __dirname !== "undefined" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
|
2988
|
+
return JSON.parse(fs.readFileSync(path.join(_dirname, "../package.json"), "utf8")).version;
|
|
2989
|
+
} catch {
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
async function checkNewVersion() {
|
|
2994
|
+
const currVersion = getVersion();
|
|
2995
|
+
let latestVersion;
|
|
2996
|
+
try {
|
|
2997
|
+
latestVersion = await getLatestVersion();
|
|
2998
|
+
} catch {
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
if (latestVersion && currVersion && semver.gt(latestVersion, currVersion)) console.log(`A newer version ${colors.cyan(latestVersion)} is available.`);
|
|
3002
|
+
}
|
|
3003
|
+
async function getLatestVersion() {
|
|
3004
|
+
const fetchResult = await fetch(`https://registry.npmjs.org/@zenstackhq/cli/${VERSION_CHECK_TAG}`, {
|
|
3005
|
+
headers: { accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*" },
|
|
3006
|
+
signal: AbortSignal.timeout(CHECK_VERSION_TIMEOUT)
|
|
3007
|
+
});
|
|
3008
|
+
if (fetchResult.ok) {
|
|
3009
|
+
const latestVersion = (await fetchResult.json())?.version;
|
|
3010
|
+
if (typeof latestVersion === "string" && semver.valid(latestVersion)) return latestVersion;
|
|
3011
|
+
}
|
|
3012
|
+
throw new Error("invalid npm registry response");
|
|
3013
|
+
}
|
|
3014
|
+
//#endregion
|
|
3015
|
+
//#region src/actions/proxy.ts
|
|
3016
|
+
async function run(options) {
|
|
3017
|
+
const allowedLogLevels = ["error", "query"];
|
|
3018
|
+
const log = options.logLevel?.filter((level) => allowedLogLevels.includes(level));
|
|
3019
|
+
const schemaFile = getSchemaFile(options.schema);
|
|
3020
|
+
console.log(colors.gray(`Loading ZModel schema from: ${schemaFile}`));
|
|
3021
|
+
let outputPath = getOutputPath(options, schemaFile);
|
|
3022
|
+
if (!path.isAbsolute(outputPath)) outputPath = path.resolve(process.cwd(), outputPath);
|
|
3023
|
+
const dataSource = (await loadSchemaDocument(schemaFile)).declarations.find(isDataSource);
|
|
3024
|
+
let databaseUrl = options.databaseUrl;
|
|
3025
|
+
if (!databaseUrl) {
|
|
3026
|
+
const schemaUrl = dataSource?.fields.find((f) => f.name === "url")?.value;
|
|
3027
|
+
if (!schemaUrl) throw new CliError(`The schema's "datasource" does not have a "url" field, please provide it with -d option.`);
|
|
3028
|
+
databaseUrl = evaluateUrl(schemaUrl);
|
|
3029
|
+
}
|
|
3030
|
+
const dialect = await createDialect(getStringLiteral(dataSource?.fields.find((f) => f.name === "provider")?.value), databaseUrl, outputPath);
|
|
3031
|
+
const schemaModule = await createJiti(typeof __filename !== "undefined" ? __filename : import.meta.url).import(path.join(outputPath, "schema"));
|
|
3032
|
+
const schema = schemaModule.schema;
|
|
3033
|
+
const omit = {};
|
|
3034
|
+
for (const [modelName, modelDef] of Object.entries(schema.models)) {
|
|
3035
|
+
const omitFields = {};
|
|
3036
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) if (fieldDef.computed === true || fieldDef.type === "Unsupported") omitFields[fieldName] = true;
|
|
3037
|
+
if (Object.keys(omitFields).length > 0) omit[modelName] = omitFields;
|
|
3038
|
+
}
|
|
3039
|
+
const db = new ZenStackClient(schema, {
|
|
3040
|
+
dialect,
|
|
3041
|
+
log: log && log.length > 0 ? log : void 0,
|
|
3042
|
+
omit: Object.keys(omit).length > 0 ? omit : void 0,
|
|
3043
|
+
skipValidationForComputedFields: true
|
|
3044
|
+
});
|
|
3045
|
+
try {
|
|
3046
|
+
await db.$connect();
|
|
3047
|
+
} catch (err) {
|
|
3048
|
+
throw new CliError(`Failed to connect to the database: ${err instanceof Error ? err.message : String(err)}`);
|
|
3049
|
+
}
|
|
3050
|
+
startServer(db, schemaModule.schema, options);
|
|
3051
|
+
}
|
|
3052
|
+
function evaluateUrl(schemaUrl) {
|
|
3053
|
+
if (isLiteralExpr(schemaUrl)) return getStringLiteral(schemaUrl);
|
|
3054
|
+
else if (isInvocationExpr(schemaUrl)) {
|
|
3055
|
+
const envName = getStringLiteral(schemaUrl.args[0]?.value);
|
|
3056
|
+
const envValue = process.env[envName];
|
|
3057
|
+
if (!envValue) throw new CliError(`Environment variable ${envName} is not set`);
|
|
3058
|
+
return envValue;
|
|
3059
|
+
} else throw new CliError(`Unable to resolve the "url" field value.`);
|
|
3060
|
+
}
|
|
3061
|
+
function redactDatabaseUrl(url) {
|
|
3062
|
+
try {
|
|
3063
|
+
const parsedUrl = new URL(url);
|
|
3064
|
+
if (parsedUrl.password) parsedUrl.password = "***";
|
|
3065
|
+
if (parsedUrl.username) parsedUrl.username = "***";
|
|
3066
|
+
return parsedUrl.toString();
|
|
3067
|
+
} catch {
|
|
3068
|
+
return url;
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
async function createDialect(provider, databaseUrl, outputPath) {
|
|
3072
|
+
switch (provider) {
|
|
3073
|
+
case "sqlite": {
|
|
3074
|
+
let SQLite;
|
|
3075
|
+
try {
|
|
3076
|
+
SQLite = (await import("better-sqlite3")).default;
|
|
3077
|
+
} catch {
|
|
3078
|
+
throw new CliError(`Package "better-sqlite3" is required for SQLite support. Please install it with: npm install better-sqlite3`);
|
|
3079
|
+
}
|
|
3080
|
+
let resolvedUrl = databaseUrl.trim();
|
|
3081
|
+
if (resolvedUrl.startsWith("file:")) {
|
|
3082
|
+
const filePath = resolvedUrl.substring(5);
|
|
3083
|
+
if (!path.isAbsolute(filePath)) resolvedUrl = path.join(outputPath, filePath);
|
|
3084
|
+
}
|
|
3085
|
+
console.log(colors.gray(`Connecting to SQLite database at: ${resolvedUrl}`));
|
|
3086
|
+
return new SqliteDialect({ database: new SQLite(resolvedUrl) });
|
|
3087
|
+
}
|
|
3088
|
+
case "postgresql": {
|
|
3089
|
+
let PgPool;
|
|
3090
|
+
try {
|
|
3091
|
+
PgPool = (await import("pg")).Pool;
|
|
3092
|
+
} catch {
|
|
3093
|
+
throw new CliError(`Package "pg" is required for PostgreSQL support. Please install it with: npm install pg`);
|
|
3094
|
+
}
|
|
3095
|
+
console.log(colors.gray(`Connecting to PostgreSQL database at: ${redactDatabaseUrl(databaseUrl)}`));
|
|
3096
|
+
return new PostgresDialect({ pool: new PgPool({ connectionString: databaseUrl }) });
|
|
3097
|
+
}
|
|
3098
|
+
case "mysql": {
|
|
3099
|
+
let createMysqlPool;
|
|
3100
|
+
try {
|
|
3101
|
+
createMysqlPool = (await import("mysql2")).createPool;
|
|
3102
|
+
} catch {
|
|
3103
|
+
throw new CliError(`Package "mysql2" is required for MySQL support. Please install it with: npm install mysql2`);
|
|
3104
|
+
}
|
|
3105
|
+
console.log(colors.gray(`Connecting to MySQL database at: ${redactDatabaseUrl(databaseUrl)}`));
|
|
3106
|
+
return new MysqlDialect({ pool: createMysqlPool(databaseUrl) });
|
|
3107
|
+
}
|
|
3108
|
+
default: throw new CliError(`Unsupported database provider: ${provider}`);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
function createProxyApp(client, schema) {
|
|
3112
|
+
const app = express();
|
|
3113
|
+
app.use(cors());
|
|
3114
|
+
app.use(express.json({ limit: "5mb" }));
|
|
3115
|
+
app.use(express.urlencoded({
|
|
3116
|
+
extended: true,
|
|
3117
|
+
limit: "5mb"
|
|
3118
|
+
}));
|
|
3119
|
+
app.use("/api/model", ZenStackMiddleware({
|
|
3120
|
+
apiHandler: new RPCApiHandler({ schema }),
|
|
3121
|
+
getClient: () => client
|
|
3122
|
+
}));
|
|
3123
|
+
app.get("/api/schema", (_req, res) => {
|
|
3124
|
+
res.json({
|
|
3125
|
+
...schema,
|
|
3126
|
+
zenstackVersion: getVersion()
|
|
3127
|
+
});
|
|
3128
|
+
});
|
|
3129
|
+
return app;
|
|
3130
|
+
}
|
|
3131
|
+
function startServer(client, schema, options) {
|
|
3132
|
+
const server = createProxyApp(client, schema).listen(options.port, () => {
|
|
3133
|
+
console.log(`ZenStack proxy server is running on port: ${options.port}`);
|
|
3134
|
+
console.log(`You can visit ZenStack Studio at: ${colors.blue("https://studio.zenstack.dev")}`);
|
|
3135
|
+
});
|
|
3136
|
+
server.on("error", (err) => {
|
|
3137
|
+
if (err.code === "EADDRINUSE") console.error(colors.red(`Port ${options.port} is already in use. Please choose a different port using -p option.`));
|
|
3138
|
+
else throw new CliError(`Failed to start the server: ${err.message}`);
|
|
3139
|
+
process.exit(1);
|
|
3140
|
+
});
|
|
3141
|
+
process.on("SIGTERM", async () => {
|
|
3142
|
+
server.close(() => {
|
|
3143
|
+
console.log("\nZenStack proxy server closed");
|
|
3144
|
+
});
|
|
3145
|
+
await client.$disconnect();
|
|
3146
|
+
process.exit(0);
|
|
3147
|
+
});
|
|
3148
|
+
process.on("SIGINT", async () => {
|
|
3149
|
+
server.close(() => {
|
|
3150
|
+
console.log("\nZenStack proxy server closed");
|
|
3151
|
+
});
|
|
3152
|
+
await client.$disconnect();
|
|
3153
|
+
process.exit(0);
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
//#endregion
|
|
3157
|
+
//#region src/constants.ts
|
|
3158
|
+
const TELEMETRY_TRACKING_TOKEN = "74944eb779d7d3b4ce185be843fde9fc";
|
|
3159
|
+
//#endregion
|
|
3160
|
+
//#region src/utils/is-ci.ts
|
|
3161
|
+
const isInCi = env["CI"] !== "0" && env["CI"] !== "false" && ("CI" in env || "CONTINUOUS_INTEGRATION" in env || Object.keys(env).some((key) => key.startsWith("CI_")));
|
|
3162
|
+
//#endregion
|
|
3163
|
+
//#region src/utils/is-docker.ts
|
|
3164
|
+
let isDockerCached;
|
|
3165
|
+
function hasDockerEnv() {
|
|
3166
|
+
try {
|
|
3167
|
+
fs.statSync("/.dockerenv");
|
|
3168
|
+
return true;
|
|
3169
|
+
} catch {
|
|
3170
|
+
return false;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
function hasDockerCGroup() {
|
|
3174
|
+
try {
|
|
3175
|
+
return fs.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
|
|
3176
|
+
} catch {
|
|
3177
|
+
return false;
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
function isDocker() {
|
|
3181
|
+
if (isDockerCached === void 0) isDockerCached = hasDockerEnv() || hasDockerCGroup();
|
|
3182
|
+
return isDockerCached;
|
|
3183
|
+
}
|
|
3184
|
+
//#endregion
|
|
3185
|
+
//#region src/utils/is-container.ts
|
|
3186
|
+
let cachedResult;
|
|
3187
|
+
const hasContainerEnv = () => {
|
|
3188
|
+
try {
|
|
3189
|
+
fs.statSync("/run/.containerenv");
|
|
3190
|
+
return true;
|
|
3191
|
+
} catch {
|
|
3192
|
+
return false;
|
|
3193
|
+
}
|
|
3194
|
+
};
|
|
3195
|
+
function isInContainer() {
|
|
3196
|
+
if (cachedResult === void 0) cachedResult = hasContainerEnv() || isDocker();
|
|
3197
|
+
return cachedResult;
|
|
3198
|
+
}
|
|
3199
|
+
//#endregion
|
|
3200
|
+
//#region src/utils/is-wsl.ts
|
|
3201
|
+
const isWsl = () => {
|
|
3202
|
+
if (process$1.platform !== "linux") return false;
|
|
3203
|
+
if (os.release().toLowerCase().includes("microsoft")) return true;
|
|
3204
|
+
try {
|
|
3205
|
+
return fs.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft");
|
|
3206
|
+
} catch {
|
|
3207
|
+
return false;
|
|
3208
|
+
}
|
|
3209
|
+
};
|
|
3210
|
+
//#endregion
|
|
3211
|
+
//#region src/utils/machine-id-utils.ts
|
|
3212
|
+
const { platform } = process;
|
|
3213
|
+
const guid = {
|
|
3214
|
+
darwin: "ioreg -rd1 -c IOPlatformExpertDevice",
|
|
3215
|
+
win32: `${{
|
|
3216
|
+
native: "%windir%\\System32",
|
|
3217
|
+
mixed: "%windir%\\sysnative\\cmd.exe /c %windir%\\System32"
|
|
3218
|
+
}[isWindowsProcessMixedOrNativeArchitecture()]}\\REG.exe QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid`,
|
|
3219
|
+
linux: "( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname 2> /dev/null) | head -n 1 || :",
|
|
3220
|
+
freebsd: "kenv -q smbios.system.uuid || sysctl -n kern.hostuuid"
|
|
3221
|
+
};
|
|
3222
|
+
function isWindowsProcessMixedOrNativeArchitecture() {
|
|
3223
|
+
if (process.arch === "ia32" && process.env.hasOwnProperty("PROCESSOR_ARCHITEW6432")) return "mixed";
|
|
3224
|
+
return "native";
|
|
3225
|
+
}
|
|
3226
|
+
function hash(guid) {
|
|
3227
|
+
return createHash("sha256").update(guid).digest("hex");
|
|
3228
|
+
}
|
|
3229
|
+
function expose(result) {
|
|
3230
|
+
switch (platform) {
|
|
3231
|
+
case "darwin": return result.split("IOPlatformUUID")[1]?.split("\n")[0]?.replace(/=|\s+|"/gi, "").toLowerCase();
|
|
3232
|
+
case "win32": return result.toString().split("REG_SZ")[1]?.replace(/\r+|\n+|\s+/gi, "").toLowerCase();
|
|
3233
|
+
case "linux": return result.toString().replace(/\r+|\n+|\s+/gi, "").toLowerCase();
|
|
3234
|
+
case "freebsd": return result.toString().replace(/\r+|\n+|\s+/gi, "").toLowerCase();
|
|
3235
|
+
default: throw new Error(`Unsupported platform: ${process.platform}`);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
function getMachineId() {
|
|
3239
|
+
if (!(platform in guid)) return randomUUID();
|
|
3240
|
+
try {
|
|
3241
|
+
const id = expose(execSync(guid[platform]).toString());
|
|
3242
|
+
if (!id) return randomUUID();
|
|
3243
|
+
return hash(id);
|
|
3244
|
+
} catch {
|
|
3245
|
+
return randomUUID();
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
//#endregion
|
|
3249
|
+
//#region src/telemetry.ts
|
|
3250
|
+
/**
|
|
3251
|
+
* Utility class for sending telemetry
|
|
3252
|
+
*/
|
|
3253
|
+
var Telemetry = class {
|
|
3254
|
+
mixpanel;
|
|
3255
|
+
hostId = getMachineId();
|
|
3256
|
+
sessionid = randomUUID();
|
|
3257
|
+
_os_type = os$1.type();
|
|
3258
|
+
_os_release = os$1.release();
|
|
3259
|
+
_os_arch = os$1.arch();
|
|
3260
|
+
_os_version = os$1.version();
|
|
3261
|
+
_os_platform = os$1.platform();
|
|
3262
|
+
version = getVersion();
|
|
3263
|
+
prismaVersion = this.getPrismaVersion();
|
|
3264
|
+
isDocker = isDocker();
|
|
3265
|
+
isWsl = isWsl();
|
|
3266
|
+
isContainer = isInContainer();
|
|
3267
|
+
isCi = isInCi;
|
|
3268
|
+
constructor() {
|
|
3269
|
+
if (process.env["DO_NOT_TRACK"] !== "1" && "<TELEMETRY_TRACKING_TOKEN>") this.mixpanel = init(TELEMETRY_TRACKING_TOKEN, { geolocate: true });
|
|
3270
|
+
}
|
|
3271
|
+
get isTracking() {
|
|
3272
|
+
return !!this.mixpanel;
|
|
3273
|
+
}
|
|
3274
|
+
track(event, properties = {}) {
|
|
3275
|
+
if (this.mixpanel) {
|
|
3276
|
+
const payload = {
|
|
3277
|
+
distinct_id: this.hostId,
|
|
3278
|
+
session: this.sessionid,
|
|
3279
|
+
time: /* @__PURE__ */ new Date(),
|
|
3280
|
+
$os: this._os_type,
|
|
3281
|
+
osType: this._os_type,
|
|
3282
|
+
osRelease: this._os_release,
|
|
3283
|
+
osPlatform: this._os_platform,
|
|
3284
|
+
osArch: this._os_arch,
|
|
3285
|
+
osVersion: this._os_version,
|
|
3286
|
+
nodeVersion: process.version,
|
|
3287
|
+
version: this.version,
|
|
3288
|
+
prismaVersion: this.prismaVersion,
|
|
3289
|
+
isDocker: this.isDocker,
|
|
3290
|
+
isWsl: this.isWsl,
|
|
3291
|
+
isContainer: this.isContainer,
|
|
3292
|
+
isCi: this.isCi,
|
|
3293
|
+
...properties
|
|
3294
|
+
};
|
|
3295
|
+
this.mixpanel.track(event, payload);
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
trackError(err) {
|
|
3299
|
+
this.track("cli:error", {
|
|
3300
|
+
message: err.message,
|
|
3301
|
+
stack: err.stack
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
async trackSpan(startEvent, completeEvent, errorEvent, properties, action) {
|
|
3305
|
+
this.track(startEvent, properties);
|
|
3306
|
+
const start = Date.now();
|
|
3307
|
+
let success = true;
|
|
3308
|
+
try {
|
|
3309
|
+
return await action();
|
|
3310
|
+
} catch (err) {
|
|
3311
|
+
this.track(errorEvent, {
|
|
3312
|
+
message: err.message,
|
|
3313
|
+
stack: err.stack,
|
|
3314
|
+
...properties
|
|
3315
|
+
});
|
|
3316
|
+
success = false;
|
|
3317
|
+
throw err;
|
|
3318
|
+
} finally {
|
|
3319
|
+
this.track(completeEvent, {
|
|
3320
|
+
duration: Date.now() - start,
|
|
3321
|
+
success,
|
|
3322
|
+
...properties
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
async trackCommand(command, action) {
|
|
3327
|
+
await this.trackSpan("cli:command:start", "cli:command:complete", "cli:command:error", { command }, action);
|
|
3328
|
+
}
|
|
3329
|
+
async trackCli(action) {
|
|
3330
|
+
await this.trackSpan("cli:start", "cli:complete", "cli:error", {}, action);
|
|
3331
|
+
}
|
|
3332
|
+
getPrismaVersion() {
|
|
3333
|
+
try {
|
|
3334
|
+
const packageJsonPath = import.meta.resolve("prisma/package.json");
|
|
3335
|
+
const packageJsonUrl = new URL(packageJsonPath);
|
|
3336
|
+
return JSON.parse(fs.readFileSync(packageJsonUrl, "utf8")).version;
|
|
3337
|
+
} catch {
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
};
|
|
3342
|
+
const telemetry = new Telemetry();
|
|
3343
|
+
//#endregion
|
|
3344
|
+
//#region src/index.ts
|
|
3345
|
+
const generateAction = async (options) => {
|
|
3346
|
+
await telemetry.trackCommand("generate", () => run$5(options));
|
|
3347
|
+
};
|
|
3348
|
+
const migrateAction = async (subCommand, options) => {
|
|
3349
|
+
await telemetry.trackCommand(`migrate ${subCommand}`, () => run$1(subCommand, options));
|
|
3350
|
+
};
|
|
3351
|
+
const dbAction = async (subCommand, options) => {
|
|
3352
|
+
await telemetry.trackCommand(`db ${subCommand}`, () => run$7(subCommand, options));
|
|
3353
|
+
};
|
|
3354
|
+
const infoAction = async (projectPath) => {
|
|
3355
|
+
await telemetry.trackCommand("info", () => run$4(projectPath));
|
|
3356
|
+
};
|
|
3357
|
+
const initAction = async (projectPath) => {
|
|
3358
|
+
await telemetry.trackCommand("init", () => run$3(projectPath));
|
|
3359
|
+
};
|
|
3360
|
+
const checkAction = async (options) => {
|
|
3361
|
+
await telemetry.trackCommand("check", () => run$8(options));
|
|
3362
|
+
};
|
|
3363
|
+
const formatAction = async (options) => {
|
|
3364
|
+
await telemetry.trackCommand("format", () => run$6(options));
|
|
3365
|
+
};
|
|
3366
|
+
const seedAction = async (options, args) => {
|
|
3367
|
+
await telemetry.trackCommand("db seed", () => run$2(options, args));
|
|
3368
|
+
};
|
|
3369
|
+
const proxyAction = async (options) => {
|
|
3370
|
+
await telemetry.trackCommand("proxy", () => run(options));
|
|
3371
|
+
};
|
|
3372
|
+
function triStateBooleanOption(flag, description) {
|
|
3373
|
+
return new Option(flag, description).choices(["true", "false"]).argParser((value) => {
|
|
3374
|
+
if (value === void 0 || value === "true") return true;
|
|
3375
|
+
if (value === "false") return false;
|
|
3376
|
+
throw new CliError(`Invalid value for ${flag}: ${value}`);
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
function createProgram() {
|
|
3380
|
+
const program = new Command("zen").alias("zenstack").helpOption("-h, --help", "Show this help message").version(getVersion(), "-v --version", "Show CLI version");
|
|
3381
|
+
const schemaExtensions = ZModelLanguageMetaData.fileExtensions.join(", ");
|
|
3382
|
+
program.description(`${colors.bold.blue("ζ")} ZenStack is the modern data layer for TypeScript apps.\n\nDocumentation: https://zenstack.dev/docs`).showHelpAfterError().showSuggestionAfterError();
|
|
3383
|
+
const schemaOption = new Option("--schema <file>", `schema file (with extension ${schemaExtensions}). Defaults to "zenstack/schema.zmodel" unless specified in package.json.`);
|
|
3384
|
+
const noVersionCheckOption = new Option("--no-version-check", "do not check for new version");
|
|
3385
|
+
const noTipsOption = new Option("--no-tips", "do not show usage tips");
|
|
3386
|
+
program.command("generate").description("Run code generation plugins").addOption(schemaOption).addOption(noVersionCheckOption).addOption(noTipsOption).addOption(new Option("-o, --output <path>", "default output directory for code generation")).addOption(new Option("-w, --watch", "enable watch mode").default(false)).addOption(triStateBooleanOption("--lite [boolean]", "also generate a lite version of schema without attributes, defaults to false")).addOption(triStateBooleanOption("--lite-only [boolean]", "only generate lite version of schema without attributes, defaults to false")).addOption(triStateBooleanOption("--generate-models [boolean]", "generate models.ts file, defaults to true")).addOption(triStateBooleanOption("--generate-input [boolean]", "generate input.ts file, defaults to true")).addOption(new Option("--silent", "suppress all output except errors").default(false)).action(generateAction);
|
|
3387
|
+
const migrateCommand = program.command("migrate").description("Run database schema migration related tasks.");
|
|
3388
|
+
const migrationsOption = new Option("--migrations <path>", "path that contains the \"migrations\" directory");
|
|
3389
|
+
migrateCommand.command("dev").addOption(schemaOption).addOption(noVersionCheckOption).addOption(new Option("-n, --name <name>", "migration name")).addOption(new Option("--create-only", "only create migration, do not apply")).addOption(migrationsOption).description("Create a migration from changes in schema and apply it to the database").action((options) => migrateAction("dev", options));
|
|
3390
|
+
migrateCommand.command("reset").addOption(schemaOption).addOption(new Option("--force", "skip the confirmation prompt")).addOption(migrationsOption).addOption(new Option("--skip-seed", "skip seeding the database after reset")).addOption(noVersionCheckOption).description("Reset your database and apply all migrations, all data will be lost").addHelpText("after", "\nIf there is a seed script defined in package.json, it will be run after the reset. Use --skip-seed to skip it.").action((options) => migrateAction("reset", options));
|
|
3391
|
+
migrateCommand.command("deploy").addOption(schemaOption).addOption(noVersionCheckOption).addOption(migrationsOption).description("Deploy your pending migrations to your production/staging database").action((options) => migrateAction("deploy", options));
|
|
3392
|
+
migrateCommand.command("status").addOption(schemaOption).addOption(noVersionCheckOption).addOption(migrationsOption).description("Check the status of your database migrations").action((options) => migrateAction("status", options));
|
|
3393
|
+
migrateCommand.command("resolve").addOption(schemaOption).addOption(noVersionCheckOption).addOption(migrationsOption).addOption(new Option("--applied <migration>", "record a specific migration as applied")).addOption(new Option("--rolled-back <migration>", "record a specific migration as rolled back")).description("Resolve issues with database migrations in deployment databases").action((options) => migrateAction("resolve", options));
|
|
3394
|
+
const dbCommand = program.command("db").description("Manage your database schema during development");
|
|
3395
|
+
dbCommand.command("push").description("Push the state from your schema to your database").addOption(schemaOption).addOption(noVersionCheckOption).addOption(new Option("--accept-data-loss", "ignore data loss warnings")).addOption(new Option("--force-reset", "force a reset of the database before push")).action((options) => dbAction("push", options));
|
|
3396
|
+
dbCommand.command("pull").description("Introspect your database.").addOption(schemaOption).addOption(noVersionCheckOption).addOption(new Option("-o, --output <path>", "set custom output path for the introspected schema. If a file path is provided, all schemas are merged into that single file. If a directory path is provided, files are written to the directory and imports are kept.")).addOption(new Option("--model-casing <pascal|camel|snake|none>", "set the casing of generated models").default("pascal")).addOption(new Option("--field-casing <pascal|camel|snake|none>", "set the casing of generated fields").default("camel")).addOption(new Option("--always-map", "always add @map and @@map attributes to models and fields").default(false)).addOption(new Option("--quote <double|single>", "set the quote style of generated schema files").default("single")).addOption(new Option("--indent <number>", "set the indentation of the generated schema files").default(4)).action((options) => dbAction("pull", options));
|
|
3397
|
+
dbCommand.command("seed").description("Seed the database").allowExcessArguments(true).addHelpText("after", `
|
|
3398
|
+
Seed script is configured under the "zenstack.seed" field in package.json.
|
|
3399
|
+
E.g.:
|
|
3400
|
+
{
|
|
3401
|
+
"zenstack": {
|
|
3402
|
+
"seed": "ts-node ./zenstack/seed.ts"
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
Arguments following -- are passed to the seed script. E.g.: "zen db seed -- --users 10"`).addOption(noVersionCheckOption).action((options, command) => seedAction(options, command.args));
|
|
3407
|
+
program.command("info").description("Get information of installed ZenStack packages").argument("[path]", "project path", ".").addOption(noVersionCheckOption).action(infoAction);
|
|
3408
|
+
program.command("init").description("Initialize an existing project for ZenStack").argument("[path]", "project path", ".").addOption(noVersionCheckOption).action(initAction);
|
|
3409
|
+
program.command("check").description("Check a ZModel schema for syntax or semantic errors").addOption(schemaOption).addOption(noVersionCheckOption).action(checkAction);
|
|
3410
|
+
program.command("format").description("Format a ZModel schema file").addOption(schemaOption).addOption(noVersionCheckOption).action(formatAction);
|
|
3411
|
+
program.command("proxy").alias("studio").description("Start the ZenStack proxy server").addOption(schemaOption).addOption(new Option("-p, --port <port>", "port to run the proxy server on").default(2311)).addOption(new Option("-o, --output <path>", "output directory for `zen generate` command")).addOption(new Option("-d, --databaseUrl <url>", "database connection URL")).addOption(new Option("-l, --logLevel <level...>", "Query log levels (e.g., query, error)")).addOption(noVersionCheckOption).action(proxyAction);
|
|
3412
|
+
program.addHelpCommand("help [command]", "Display help for a command");
|
|
3413
|
+
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
3414
|
+
if (actionCommand.getOptionValue("versionCheck") !== false) await checkNewVersion();
|
|
3415
|
+
});
|
|
3416
|
+
return program;
|
|
3417
|
+
}
|
|
3418
|
+
async function main() {
|
|
3419
|
+
let exitCode = 0;
|
|
3420
|
+
const program = createProgram();
|
|
3421
|
+
program.exitOverride();
|
|
3422
|
+
try {
|
|
3423
|
+
await telemetry.trackCli(async () => {
|
|
3424
|
+
await program.parseAsync();
|
|
3425
|
+
});
|
|
3426
|
+
} catch (e) {
|
|
3427
|
+
if (e instanceof CommanderError) exitCode = e.exitCode;
|
|
3428
|
+
else if (e instanceof CliError) {
|
|
3429
|
+
console.error(colors.red(e.message));
|
|
3430
|
+
exitCode = 1;
|
|
3431
|
+
} else {
|
|
3432
|
+
console.error(colors.red(`Unhandled error: ${e}`));
|
|
3433
|
+
exitCode = 1;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
if (program.args.includes("generate") && (program.args.includes("-w") || program.args.includes("--watch")) || ["proxy", "studio"].some((cmd) => program.args.includes(cmd))) return;
|
|
3437
|
+
if (telemetry.isTracking) setTimeout(() => {
|
|
3438
|
+
process.exit(exitCode);
|
|
3439
|
+
}, 200);
|
|
3440
|
+
else process.exit(exitCode);
|
|
3441
|
+
}
|
|
3442
|
+
main();
|
|
3443
|
+
//#endregion
|
|
3444
|
+
export {};
|
|
3445
|
+
|
|
3446
|
+
//# sourceMappingURL=index.mjs.map
|