alepha 0.11.7 → 0.11.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -17
- package/dist/index.cjs +15805 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15804 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -372
- package/src/assets/biomeJson.ts +33 -0
- package/src/assets/tsconfigJson.ts +17 -0
- package/src/assets/viteConfigTs.ts +14 -0
- package/src/commands/BiomeCommands.ts +60 -0
- package/src/commands/CoreCommands.ts +266 -0
- package/src/commands/DrizzleCommands.ts +403 -0
- package/src/commands/VerifyCommands.ts +48 -0
- package/src/commands/ViteCommands.ts +152 -0
- package/src/index.ts +35 -0
- package/src/services/ProcessRunner.ts +89 -0
- package/src/version.ts +7 -0
- package/api/files.cjs +0 -8
- package/api/files.d.ts +0 -438
- package/api/files.js +0 -1
- package/api/jobs.cjs +0 -8
- package/api/jobs.d.ts +0 -327
- package/api/jobs.js +0 -1
- package/api/notifications.cjs +0 -8
- package/api/notifications.d.ts +0 -263
- package/api/notifications.js +0 -1
- package/api/users.cjs +0 -8
- package/api/users.d.ts +0 -923
- package/api/users.js +0 -1
- package/api/verifications.cjs +0 -8
- package/api/verifications.d.ts +0 -1
- package/api/verifications.js +0 -1
- package/batch.cjs +0 -8
- package/batch.d.ts +0 -154
- package/batch.js +0 -1
- package/bucket.cjs +0 -8
- package/bucket.d.ts +0 -520
- package/bucket.js +0 -1
- package/cache/redis.cjs +0 -8
- package/cache/redis.d.ts +0 -40
- package/cache/redis.js +0 -1
- package/cache.cjs +0 -8
- package/cache.d.ts +0 -288
- package/cache.js +0 -1
- package/command.cjs +0 -8
- package/command.d.ts +0 -269
- package/command.js +0 -1
- package/core.cjs +0 -8
- package/core.d.ts +0 -1904
- package/core.js +0 -1
- package/datetime.cjs +0 -8
- package/datetime.d.ts +0 -144
- package/datetime.js +0 -1
- package/devtools.cjs +0 -8
- package/devtools.d.ts +0 -252
- package/devtools.js +0 -1
- package/email.cjs +0 -8
- package/email.d.ts +0 -187
- package/email.js +0 -1
- package/fake.cjs +0 -8
- package/fake.d.ts +0 -73
- package/fake.js +0 -1
- package/file.cjs +0 -8
- package/file.d.ts +0 -528
- package/file.js +0 -1
- package/lock/redis.cjs +0 -8
- package/lock/redis.d.ts +0 -24
- package/lock/redis.js +0 -1
- package/lock.cjs +0 -8
- package/lock.d.ts +0 -552
- package/lock.js +0 -1
- package/logger.cjs +0 -8
- package/logger.d.ts +0 -287
- package/logger.js +0 -1
- package/postgres.cjs +0 -8
- package/postgres.d.ts +0 -2143
- package/postgres.js +0 -1
- package/queue/redis.cjs +0 -8
- package/queue/redis.d.ts +0 -29
- package/queue/redis.js +0 -1
- package/queue.cjs +0 -8
- package/queue.d.ts +0 -760
- package/queue.js +0 -1
- package/react/auth.cjs +0 -8
- package/react/auth.d.ts +0 -504
- package/react/auth.js +0 -1
- package/react/form.cjs +0 -8
- package/react/form.d.ts +0 -211
- package/react/form.js +0 -1
- package/react/head.cjs +0 -8
- package/react/head.d.ts +0 -120
- package/react/head.js +0 -1
- package/react/i18n.cjs +0 -8
- package/react/i18n.d.ts +0 -168
- package/react/i18n.js +0 -1
- package/react.cjs +0 -8
- package/react.d.ts +0 -1263
- package/react.js +0 -1
- package/redis.cjs +0 -8
- package/redis.d.ts +0 -82
- package/redis.js +0 -1
- package/retry.cjs +0 -8
- package/retry.d.ts +0 -162
- package/retry.js +0 -1
- package/router.cjs +0 -8
- package/router.d.ts +0 -45
- package/router.js +0 -1
- package/scheduler.cjs +0 -8
- package/scheduler.d.ts +0 -145
- package/scheduler.js +0 -1
- package/security.cjs +0 -8
- package/security.d.ts +0 -586
- package/security.js +0 -1
- package/server/cache.cjs +0 -8
- package/server/cache.d.ts +0 -163
- package/server/cache.js +0 -1
- package/server/compress.cjs +0 -8
- package/server/compress.d.ts +0 -38
- package/server/compress.js +0 -1
- package/server/cookies.cjs +0 -8
- package/server/cookies.d.ts +0 -144
- package/server/cookies.js +0 -1
- package/server/cors.cjs +0 -8
- package/server/cors.d.ts +0 -45
- package/server/cors.js +0 -1
- package/server/health.cjs +0 -8
- package/server/health.d.ts +0 -58
- package/server/health.js +0 -1
- package/server/helmet.cjs +0 -8
- package/server/helmet.d.ts +0 -98
- package/server/helmet.js +0 -1
- package/server/links.cjs +0 -8
- package/server/links.d.ts +0 -322
- package/server/links.js +0 -1
- package/server/metrics.cjs +0 -8
- package/server/metrics.d.ts +0 -35
- package/server/metrics.js +0 -1
- package/server/multipart.cjs +0 -8
- package/server/multipart.d.ts +0 -42
- package/server/multipart.js +0 -1
- package/server/proxy.cjs +0 -8
- package/server/proxy.d.ts +0 -234
- package/server/proxy.js +0 -1
- package/server/security.cjs +0 -8
- package/server/security.d.ts +0 -92
- package/server/security.js +0 -1
- package/server/static.cjs +0 -8
- package/server/static.d.ts +0 -119
- package/server/static.js +0 -1
- package/server/swagger.cjs +0 -8
- package/server/swagger.d.ts +0 -161
- package/server/swagger.js +0 -1
- package/server.cjs +0 -8
- package/server.d.ts +0 -849
- package/server.js +0 -1
- package/topic/redis.cjs +0 -8
- package/topic/redis.d.ts +0 -42
- package/topic/redis.js +0 -1
- package/topic.cjs +0 -8
- package/topic.d.ts +0 -819
- package/topic.js +0 -1
- package/ui.cjs +0 -8
- package/ui.d.ts +0 -813
- package/ui.js +0 -1
- package/vite.cjs +0 -8
- package/vite.d.ts +0 -186
- package/vite.js +0 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { $command, CliProvider } from "@alepha/command";
|
|
5
|
+
import { $inject, AlephaError, t } from "@alepha/core";
|
|
6
|
+
import { $logger } from "@alepha/logger";
|
|
7
|
+
import { pipeline } from "stream/promises";
|
|
8
|
+
import * as tar from "tar";
|
|
9
|
+
import { tsconfigJson } from "../assets/tsconfigJson.ts";
|
|
10
|
+
import { version } from "../version.ts";
|
|
11
|
+
|
|
12
|
+
export class CoreCommands {
|
|
13
|
+
protected readonly log = $logger();
|
|
14
|
+
protected readonly cli = $inject(CliProvider);
|
|
15
|
+
|
|
16
|
+
public readonly root = $command({
|
|
17
|
+
root: true,
|
|
18
|
+
flags: t.object({
|
|
19
|
+
version: t.optional(
|
|
20
|
+
t.boolean({
|
|
21
|
+
description: "Show Alepha CLI version",
|
|
22
|
+
aliases: ["v"],
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
}),
|
|
26
|
+
handler: async ({ flags }) => {
|
|
27
|
+
if (flags.version) {
|
|
28
|
+
this.log.info(version);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.cli.printHelp();
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
public readonly create = $command({
|
|
37
|
+
name: "create",
|
|
38
|
+
description: "Create a new Alepha project",
|
|
39
|
+
aliases: ["new"],
|
|
40
|
+
args: t.text({
|
|
41
|
+
title: "name",
|
|
42
|
+
}),
|
|
43
|
+
flags: t.object({
|
|
44
|
+
yarn: t.optional(t.boolean({ description: "Use Yarn package manager" })),
|
|
45
|
+
pnpm: t.optional(t.boolean({ description: "Use pnpm package manager" })),
|
|
46
|
+
}),
|
|
47
|
+
summary: false,
|
|
48
|
+
handler: async ({ run, args, flags }) => {
|
|
49
|
+
const name = args;
|
|
50
|
+
const root = process.cwd();
|
|
51
|
+
const dest = join(root, name);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await access(dest);
|
|
55
|
+
this.log.error(
|
|
56
|
+
`Directory "${name}" already exists. Please choose a different project name.`,
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
} catch {
|
|
60
|
+
// Directory does not exist, proceed
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let installCmd = "npm install";
|
|
64
|
+
let execCmd = "npx";
|
|
65
|
+
if (flags.yarn) {
|
|
66
|
+
installCmd = "yarn";
|
|
67
|
+
execCmd = "yarn";
|
|
68
|
+
} else if (flags.pnpm) {
|
|
69
|
+
installCmd = "pnpm install";
|
|
70
|
+
execCmd = "pnpm";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await mkdir(dest, { recursive: true }).catch(() => null);
|
|
74
|
+
|
|
75
|
+
await run("Downloading sample project", () =>
|
|
76
|
+
this.downloadSampleProject(dest),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (flags.yarn) {
|
|
80
|
+
await this.ensureYarn(dest);
|
|
81
|
+
await run(`cd ${name} && yarn set version stable`, {
|
|
82
|
+
alias: "Setting Yarn to stable version",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await run(`cd ${name} && ${installCmd}`, {
|
|
87
|
+
alias: "Installing dependencies",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await run(`cd ${name} && npx alepha lint`, {
|
|
91
|
+
alias: "Linting code",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await run(`cd ${name} && npx alepha typecheck`, {
|
|
95
|
+
alias: "Type checking",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await run(`cd ${name} && npx alepha test`, {
|
|
99
|
+
alias: "Running tests",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await run(`cd ${name} && npx alepha build`, {
|
|
103
|
+
alias: "Building project",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
this.log.info("");
|
|
107
|
+
this.log.info(`$ cd ${name} && ${execCmd} alepha dev`.trim());
|
|
108
|
+
this.log.info("");
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
public readonly clean = $command({
|
|
113
|
+
name: "clean",
|
|
114
|
+
description: "Clean the project",
|
|
115
|
+
handler: async ({ run }) => {
|
|
116
|
+
await run.rm("./dist");
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
public readonly init = $command({
|
|
121
|
+
name: "init",
|
|
122
|
+
description: "Add missing Alepha configuration files to the project",
|
|
123
|
+
flags: t.object({
|
|
124
|
+
// TODO:
|
|
125
|
+
// force: t.boolean({
|
|
126
|
+
// description: "If true, all config files will be overwritten",
|
|
127
|
+
// }),
|
|
128
|
+
yarn: t.boolean({ description: "Use Yarn package manager" }),
|
|
129
|
+
api: t.boolean({ description: "Include Alepha Server dependencies" }),
|
|
130
|
+
react: t.boolean({ description: "Include Alepha React dependencies" }),
|
|
131
|
+
}),
|
|
132
|
+
handler: async ({ run, flags }) => {
|
|
133
|
+
const root = process.cwd();
|
|
134
|
+
|
|
135
|
+
await this.ensureTsConfig(root);
|
|
136
|
+
await this.ensurePackageJson(root, flags);
|
|
137
|
+
|
|
138
|
+
if (flags.yarn) {
|
|
139
|
+
await this.ensureYarn(root);
|
|
140
|
+
} else {
|
|
141
|
+
await run("npm install", {
|
|
142
|
+
alias: "Installing dependencies with npm",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
public async ensureYarn(root: string) {
|
|
149
|
+
const tsconfigPath = join(root, ".yarnrc.yml");
|
|
150
|
+
try {
|
|
151
|
+
await access(tsconfigPath);
|
|
152
|
+
} catch {
|
|
153
|
+
await writeFile(tsconfigPath, "nodeLinker: node-modules");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public generatePackageJsonContent(modes: { api?: boolean; react?: boolean }) {
|
|
158
|
+
const dependencies: Record<string, string> = {
|
|
159
|
+
"@alepha/core": `^${version}`,
|
|
160
|
+
"@alepha/logger": `^${version}`,
|
|
161
|
+
"@alepha/datetime": `^${version}`,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const devDependencies: Record<string, string> = {
|
|
165
|
+
alepha: `^${version}`,
|
|
166
|
+
"@alepha/vite": `^${version}`,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (modes.api) {
|
|
170
|
+
dependencies["@alepha/server"] = `^${version}`;
|
|
171
|
+
dependencies["@alepha/server-swagger"] = `^${version}`;
|
|
172
|
+
dependencies["@alepha/server-multipart"] = `^${version}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (modes.react) {
|
|
176
|
+
dependencies["@alepha/server"] = `^${version}`;
|
|
177
|
+
dependencies["@alepha/server-links"] = `^${version}`;
|
|
178
|
+
dependencies["@alepha/react"] = `^${version}`;
|
|
179
|
+
dependencies.react = "^19.2.0";
|
|
180
|
+
devDependencies["@types/react"] = "^19.0.0";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
dependencies,
|
|
185
|
+
devDependencies,
|
|
186
|
+
scripts: {
|
|
187
|
+
dev: "alepha dev",
|
|
188
|
+
build: "alepha build",
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public async ensurePackageJson(
|
|
194
|
+
root: string,
|
|
195
|
+
modes: { api?: boolean; react?: boolean },
|
|
196
|
+
) {
|
|
197
|
+
const packageJsonPath = join(root, "package.json");
|
|
198
|
+
try {
|
|
199
|
+
await access(packageJsonPath);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
this.log.info("No package.json found. Creating one...");
|
|
202
|
+
await writeFile(
|
|
203
|
+
packageJsonPath,
|
|
204
|
+
JSON.stringify(this.generatePackageJsonContent(modes), null, 2),
|
|
205
|
+
);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const content = await readFile(packageJsonPath, "utf8");
|
|
210
|
+
const packageJson = JSON.parse(content);
|
|
211
|
+
if (!packageJson.type || packageJson.type !== "module") {
|
|
212
|
+
packageJson.type = "module";
|
|
213
|
+
}
|
|
214
|
+
const newPackageJson = this.generatePackageJsonContent(modes);
|
|
215
|
+
|
|
216
|
+
packageJson.type = "module";
|
|
217
|
+
packageJson.dependencies ??= {};
|
|
218
|
+
packageJson.devDependencies ??= {};
|
|
219
|
+
packageJson.scripts ??= {};
|
|
220
|
+
|
|
221
|
+
Object.assign(packageJson.dependencies, newPackageJson.dependencies);
|
|
222
|
+
Object.assign(packageJson.devDependencies, newPackageJson.devDependencies);
|
|
223
|
+
Object.assign(packageJson.scripts, newPackageJson.scripts);
|
|
224
|
+
|
|
225
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public async ensureTsConfig(root = process.cwd()) {
|
|
229
|
+
const tsconfigPath = join(root, "tsconfig.json");
|
|
230
|
+
try {
|
|
231
|
+
await access(tsconfigPath);
|
|
232
|
+
} catch {
|
|
233
|
+
this.log.info("Missing tsconfig.json detected. Creating one...");
|
|
234
|
+
await writeFile(tsconfigPath, tsconfigJson);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public async downloadSampleProject(targetDir: string) {
|
|
239
|
+
const url = "https://api.github.com/repos/feunard/alepha/tarball/main";
|
|
240
|
+
const response = await fetch(url, {
|
|
241
|
+
headers: {
|
|
242
|
+
"User-Agent": "Alepha-CLI", // GitHub API requires User-Agent
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
throw new AlephaError(`Failed to download: ${response.statusText}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const tarStream = Readable.fromWeb(response.body as any);
|
|
251
|
+
await pipeline(
|
|
252
|
+
tarStream,
|
|
253
|
+
tar.extract({
|
|
254
|
+
cwd: targetDir, // Extract to target directory
|
|
255
|
+
strip: 3, // Remove feunard-alepha-<hash>/apps/starter prefix
|
|
256
|
+
filter: (path) => {
|
|
257
|
+
// Only extract files from apps/starter/
|
|
258
|
+
const parts = path.split("/");
|
|
259
|
+
return (
|
|
260
|
+
parts.length >= 3 && parts[1] === "apps" && parts[2] === "starter"
|
|
261
|
+
);
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { $command } from "@alepha/command";
|
|
5
|
+
import { $inject, Alepha, AlephaError, boot, t } from "@alepha/core";
|
|
6
|
+
import { $logger } from "@alepha/logger";
|
|
7
|
+
import type { RepositoryProvider } from "@alepha/postgres";
|
|
8
|
+
import { tsImport } from "tsx/esm/api";
|
|
9
|
+
import { ProcessRunner } from "../services/ProcessRunner.ts";
|
|
10
|
+
|
|
11
|
+
export class DrizzleCommands {
|
|
12
|
+
log = $logger();
|
|
13
|
+
runner = $inject(ProcessRunner);
|
|
14
|
+
|
|
15
|
+
flags = t.object({
|
|
16
|
+
root: t.text({ description: "Project root", default: "." }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if database migrations are up to date
|
|
21
|
+
*
|
|
22
|
+
* - Loads the Alepha instance from the specified entry file.
|
|
23
|
+
* - Retrieves all repository descriptors to gather database models.
|
|
24
|
+
* - Reads the last migration snapshot from the migration journal.
|
|
25
|
+
* - Generates the current database schema representation.
|
|
26
|
+
* - Compares the current schema with the last snapshot to detect changes.
|
|
27
|
+
* - If changes are detected, prompts the user to run the migration generation command!
|
|
28
|
+
*/
|
|
29
|
+
check = $command({
|
|
30
|
+
name: "db:check-migrations",
|
|
31
|
+
description: "Verify database migration files are up to date",
|
|
32
|
+
flags: this.flags,
|
|
33
|
+
args: t.optional(
|
|
34
|
+
t.text({
|
|
35
|
+
title: "path",
|
|
36
|
+
description: "Path to the Alepha server entry file",
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
handler: async ({ flags, args }) => {
|
|
40
|
+
const rootDir = join(process.cwd(), flags.root);
|
|
41
|
+
this.log.debug(`Using project root: ${rootDir}`);
|
|
42
|
+
const { alepha } = await this.loadAlephaFromServerEntryFile(
|
|
43
|
+
rootDir,
|
|
44
|
+
args,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const models: any[] = [];
|
|
48
|
+
const repositories = alepha.descriptors("repository") as any[];
|
|
49
|
+
const kit = createRequire(import.meta.url)("drizzle-kit/api");
|
|
50
|
+
const migrationDir = join(rootDir, "migrations");
|
|
51
|
+
|
|
52
|
+
const journalFile = await readFile(
|
|
53
|
+
`${migrationDir}/meta/_journal.json`,
|
|
54
|
+
"utf-8",
|
|
55
|
+
).catch(() => null);
|
|
56
|
+
|
|
57
|
+
if (!journalFile) {
|
|
58
|
+
this.log.info(`No migration journal found.`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const journal = JSON.parse(journalFile);
|
|
63
|
+
|
|
64
|
+
const lastMigration = journal.entries[journal.entries.length - 1];
|
|
65
|
+
|
|
66
|
+
const lastSnapshot = JSON.parse(
|
|
67
|
+
await readFile(
|
|
68
|
+
`${migrationDir}/meta/${String(lastMigration.idx).padStart(4, "0")}_snapshot.json`,
|
|
69
|
+
"utf-8",
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
for (const repository of repositories) {
|
|
74
|
+
if (!models.includes(repository.table)) {
|
|
75
|
+
models.push(repository.table);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const now = kit.generateDrizzleJson(models, lastSnapshot.id);
|
|
80
|
+
|
|
81
|
+
const migrationStatements = await new Promise<Array<any>>((resolve) => {
|
|
82
|
+
(async () => {
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
resolve([{ message: "Migration generation timed out." }]);
|
|
85
|
+
}, 5000);
|
|
86
|
+
const statements = await kit.generateMigration(lastSnapshot, now);
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
resolve(statements);
|
|
89
|
+
})();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (migrationStatements.length === 0) {
|
|
93
|
+
this.log.info("No changes detected.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.log.info("");
|
|
98
|
+
this.log.info("Detected migration statements:");
|
|
99
|
+
this.log.info("");
|
|
100
|
+
for (const stmt of migrationStatements) {
|
|
101
|
+
this.log.info(stmt);
|
|
102
|
+
}
|
|
103
|
+
this.log.info("");
|
|
104
|
+
|
|
105
|
+
this.log.info(
|
|
106
|
+
`At least ${migrationStatements.length} change(s) detected.`,
|
|
107
|
+
);
|
|
108
|
+
this.log.info(
|
|
109
|
+
"Please, run 'alepha db:generate' to update the migration files.",
|
|
110
|
+
);
|
|
111
|
+
this.log.info("");
|
|
112
|
+
|
|
113
|
+
throw new AlephaError("Database migrations are not up to date.");
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate database migration files
|
|
119
|
+
*
|
|
120
|
+
* - Loads the Alepha instance from the specified entry file.
|
|
121
|
+
* - Retrieves all repository descriptors to gather database models.
|
|
122
|
+
* - Creates temporary entity definitions based on the current database schema.
|
|
123
|
+
* - Writes these definitions to a temporary schema file. (node_modules/.db/entities.ts)
|
|
124
|
+
* - Invokes Drizzle Kit's CLI to generate migration files based on the current schema.
|
|
125
|
+
*/
|
|
126
|
+
generate = $command({
|
|
127
|
+
name: "db:generate",
|
|
128
|
+
description: "Generate migration files based on current database schema",
|
|
129
|
+
summary: false,
|
|
130
|
+
flags: this.flags,
|
|
131
|
+
args: t.optional(
|
|
132
|
+
t.text({
|
|
133
|
+
title: "path",
|
|
134
|
+
description: "Path to the Alepha server entry file",
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
handler: async ({ flags, args }) => {
|
|
138
|
+
await this.runDrizzleKitCommand({
|
|
139
|
+
flags,
|
|
140
|
+
args,
|
|
141
|
+
command: "generate",
|
|
142
|
+
logMessage: (providerName, dialect) =>
|
|
143
|
+
`Generate '${providerName}' migrations (${dialect}) ...`,
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Push database schema changes directly to the database
|
|
150
|
+
*
|
|
151
|
+
* - Loads the Alepha instance from the specified entry file.
|
|
152
|
+
* - Retrieves all repository descriptors to gather database models.
|
|
153
|
+
* - Creates temporary entity definitions and Drizzle config.
|
|
154
|
+
* - Invokes Drizzle Kit's push command to apply schema changes directly.
|
|
155
|
+
*/
|
|
156
|
+
push = $command({
|
|
157
|
+
name: "db:push",
|
|
158
|
+
description: "Push database schema changes directly to the database",
|
|
159
|
+
summary: false,
|
|
160
|
+
flags: this.flags,
|
|
161
|
+
args: t.optional(
|
|
162
|
+
t.text({
|
|
163
|
+
title: "path",
|
|
164
|
+
description: "Path to the Alepha server entry file",
|
|
165
|
+
}),
|
|
166
|
+
),
|
|
167
|
+
handler: async ({ flags, args }) => {
|
|
168
|
+
await this.runDrizzleKitCommand({
|
|
169
|
+
flags,
|
|
170
|
+
args,
|
|
171
|
+
command: "push",
|
|
172
|
+
logMessage: (providerName, dialect) =>
|
|
173
|
+
`Push '${providerName}' schema (${dialect}) ...`,
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Apply pending database migrations
|
|
180
|
+
*
|
|
181
|
+
* - Loads the Alepha instance from the specified entry file.
|
|
182
|
+
* - Retrieves all repository descriptors to gather database models.
|
|
183
|
+
* - Creates temporary entity definitions and Drizzle config.
|
|
184
|
+
* - Invokes Drizzle Kit's migrate command to apply pending migrations.
|
|
185
|
+
*/
|
|
186
|
+
migrate = $command({
|
|
187
|
+
name: "db:migrate",
|
|
188
|
+
description: "Apply pending database migrations",
|
|
189
|
+
summary: false,
|
|
190
|
+
flags: this.flags,
|
|
191
|
+
args: t.optional(
|
|
192
|
+
t.text({
|
|
193
|
+
title: "path",
|
|
194
|
+
description: "Path to the Alepha server entry file",
|
|
195
|
+
}),
|
|
196
|
+
),
|
|
197
|
+
handler: async ({ flags, args }) => {
|
|
198
|
+
await this.runDrizzleKitCommand({
|
|
199
|
+
flags,
|
|
200
|
+
args,
|
|
201
|
+
command: "migrate",
|
|
202
|
+
logMessage: (providerName, dialect) =>
|
|
203
|
+
`Migrate '${providerName}' database (${dialect}) ...`,
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Launch Drizzle Studio database browser
|
|
210
|
+
*
|
|
211
|
+
* - Loads the Alepha instance from the specified entry file.
|
|
212
|
+
* - Retrieves all repository descriptors to gather database models.
|
|
213
|
+
* - Creates temporary entity definitions and Drizzle config.
|
|
214
|
+
* - Invokes Drizzle Kit's studio command to launch the web-based database browser.
|
|
215
|
+
*/
|
|
216
|
+
studio = $command({
|
|
217
|
+
name: "db:studio",
|
|
218
|
+
description: "Launch Drizzle Studio database browser",
|
|
219
|
+
summary: false,
|
|
220
|
+
flags: this.flags,
|
|
221
|
+
args: t.optional(
|
|
222
|
+
t.text({
|
|
223
|
+
title: "path",
|
|
224
|
+
description: "Path to the Alepha server entry file",
|
|
225
|
+
}),
|
|
226
|
+
),
|
|
227
|
+
handler: async ({ flags, args }) => {
|
|
228
|
+
await this.runDrizzleKitCommand({
|
|
229
|
+
flags,
|
|
230
|
+
args,
|
|
231
|
+
command: "studio",
|
|
232
|
+
logMessage: (providerName, dialect) =>
|
|
233
|
+
`Launch Studio for '${providerName}' (${dialect}) ...`,
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Run a drizzle-kit command for all database providers
|
|
240
|
+
*/
|
|
241
|
+
protected async runDrizzleKitCommand(options: {
|
|
242
|
+
flags: { root: string };
|
|
243
|
+
args?: string;
|
|
244
|
+
command: string;
|
|
245
|
+
logMessage: (providerName: string, dialect: string) => string;
|
|
246
|
+
}): Promise<void> {
|
|
247
|
+
const rootDir = join(process.cwd(), options.flags.root);
|
|
248
|
+
this.log.debug(`Using project root: ${rootDir}`);
|
|
249
|
+
|
|
250
|
+
const { alepha, entry } = await this.loadAlephaFromServerEntryFile(
|
|
251
|
+
rootDir,
|
|
252
|
+
options.args,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const kit = this.getKitFromAlepha(alepha);
|
|
256
|
+
const repositoryProvider =
|
|
257
|
+
alepha.inject<RepositoryProvider>("RepositoryProvider");
|
|
258
|
+
const accepted = new Set<string>([]);
|
|
259
|
+
|
|
260
|
+
for (const descriptor of repositoryProvider.getRepositories()) {
|
|
261
|
+
const provider = descriptor.provider;
|
|
262
|
+
const providerName = provider.name;
|
|
263
|
+
const dialect = provider.dialect;
|
|
264
|
+
|
|
265
|
+
if (accepted.has(providerName)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
accepted.add(providerName);
|
|
269
|
+
|
|
270
|
+
this.log.info("");
|
|
271
|
+
this.log.info(options.logMessage(providerName, dialect));
|
|
272
|
+
|
|
273
|
+
const drizzleConfigJsPath = await this.prepareDrizzleConfig({
|
|
274
|
+
kit,
|
|
275
|
+
provider,
|
|
276
|
+
providerName,
|
|
277
|
+
providerUrl: provider.url,
|
|
278
|
+
dialect,
|
|
279
|
+
entry,
|
|
280
|
+
rootDir,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await this.runner.exec(
|
|
284
|
+
`drizzle-kit ${options.command} --config=${drizzleConfigJsPath}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Prepare Drizzle configuration files for a provider
|
|
291
|
+
*/
|
|
292
|
+
protected async prepareDrizzleConfig(options: {
|
|
293
|
+
kit: any;
|
|
294
|
+
provider: any;
|
|
295
|
+
providerName: string;
|
|
296
|
+
providerUrl: string;
|
|
297
|
+
dialect: string;
|
|
298
|
+
entry: string;
|
|
299
|
+
rootDir: string;
|
|
300
|
+
}): Promise<string> {
|
|
301
|
+
const models = Object.keys(options.kit.getModels(options.provider));
|
|
302
|
+
const entitiesJs = this.generateEntitiesJs(
|
|
303
|
+
options.entry,
|
|
304
|
+
options.providerName,
|
|
305
|
+
models,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const entitiesJsPath = await this.runner.writeConfigFile(
|
|
309
|
+
"entities.js",
|
|
310
|
+
entitiesJs,
|
|
311
|
+
options.rootDir,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const config: Record<string, any> = {
|
|
315
|
+
schema: entitiesJsPath,
|
|
316
|
+
out: `./migrations/${options.providerName}`,
|
|
317
|
+
dialect: options.dialect,
|
|
318
|
+
dbCredentials: {
|
|
319
|
+
url: options.providerUrl,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
if (options.providerName === "pglite") {
|
|
324
|
+
config.driver = "pglite";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const drizzleConfigJs = "export default " + JSON.stringify(config, null, 2);
|
|
328
|
+
|
|
329
|
+
return await this.runner.writeConfigFile(
|
|
330
|
+
"drizzle.config.js",
|
|
331
|
+
drizzleConfigJs,
|
|
332
|
+
options.rootDir,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get DrizzleKitProvider from Alepha instance
|
|
338
|
+
*/
|
|
339
|
+
protected getKitFromAlepha(alepha: Alepha): any {
|
|
340
|
+
// biome-ignore lint/complexity/useLiteralKeys: private key
|
|
341
|
+
return alepha["registry"]
|
|
342
|
+
.values()
|
|
343
|
+
.find((it: any) => it.instance.constructor.name === "DrizzleKitProvider")
|
|
344
|
+
?.instance;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
public async loadAlephaFromServerEntryFile(
|
|
348
|
+
rootDir?: string,
|
|
349
|
+
explicitEntry?: string,
|
|
350
|
+
): Promise<{
|
|
351
|
+
alepha: Alepha;
|
|
352
|
+
entry: string;
|
|
353
|
+
}> {
|
|
354
|
+
process.env.ALEPHA_SKIP_START = "true";
|
|
355
|
+
|
|
356
|
+
const entry = await boot.getServerEntry(rootDir, explicitEntry);
|
|
357
|
+
const mod = await tsImport(entry, {
|
|
358
|
+
parentURL: import.meta.url,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
this.log.debug(`Load entry: ${entry}`);
|
|
362
|
+
|
|
363
|
+
// check if alepha is correctly exported
|
|
364
|
+
if (mod.default instanceof Alepha) {
|
|
365
|
+
return {
|
|
366
|
+
alepha: mod.default,
|
|
367
|
+
entry,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// else, try with global variable
|
|
372
|
+
const g: any = global;
|
|
373
|
+
if (g.__alepha) {
|
|
374
|
+
return {
|
|
375
|
+
alepha: g.__alepha,
|
|
376
|
+
entry,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
throw new AlephaError(
|
|
381
|
+
`Could not find Alepha instance in entry file: ${entry}`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
protected generateEntitiesJs(
|
|
386
|
+
entry: string,
|
|
387
|
+
provider: string,
|
|
388
|
+
models: string[] = [],
|
|
389
|
+
) {
|
|
390
|
+
return `
|
|
391
|
+
import "${entry}";
|
|
392
|
+
import { DrizzleKitProvider, Repository } from "@alepha/postgres";
|
|
393
|
+
|
|
394
|
+
const alepha = globalThis.__alepha;
|
|
395
|
+
const kit = alepha.inject(DrizzleKitProvider);
|
|
396
|
+
const provider = alepha.services(Repository).find((it) => it.provider.name === "${provider}").provider;
|
|
397
|
+
const models = kit.getModels(provider);
|
|
398
|
+
|
|
399
|
+
${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")}
|
|
400
|
+
|
|
401
|
+
`.trim();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { $command } from "@alepha/command";
|
|
2
|
+
import { $inject } from "@alepha/core";
|
|
3
|
+
import { ProcessRunner } from "../services/ProcessRunner.ts";
|
|
4
|
+
|
|
5
|
+
export class VerifyCommands {
|
|
6
|
+
runner = $inject(ProcessRunner);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run a series of verification commands to ensure code quality and correctness.
|
|
10
|
+
*
|
|
11
|
+
* This command runs the following checks in order:
|
|
12
|
+
* 1. Clean the project
|
|
13
|
+
* 2. Format the code
|
|
14
|
+
* 3. Lint the code
|
|
15
|
+
* 4. Run tests
|
|
16
|
+
* 5. Type check the code
|
|
17
|
+
* 8. Build the project
|
|
18
|
+
* 9. Clean the project again
|
|
19
|
+
*/
|
|
20
|
+
public readonly verify = $command({
|
|
21
|
+
name: "verify",
|
|
22
|
+
description: "Verify the Alepha project",
|
|
23
|
+
handler: async ({ run }) => {
|
|
24
|
+
await run("alepha clean");
|
|
25
|
+
await run("alepha format");
|
|
26
|
+
await run("alepha lint");
|
|
27
|
+
await run("alepha test");
|
|
28
|
+
await run("alepha typecheck");
|
|
29
|
+
|
|
30
|
+
// run only if migrations dir is present ?
|
|
31
|
+
//await run("alepha db:check-migrations");
|
|
32
|
+
|
|
33
|
+
await run("alepha build");
|
|
34
|
+
await run("alepha clean");
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run TypeScript type checking across the codebase with no emit.
|
|
40
|
+
*/
|
|
41
|
+
public readonly typecheck = $command({
|
|
42
|
+
name: "typecheck",
|
|
43
|
+
description: "Check TypeScript types across the codebase",
|
|
44
|
+
handler: async () => {
|
|
45
|
+
await this.runner.exec("tsc --noEmit");
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|