@zerotal/core 1.0.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/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +72 -0
- package/src/application/Application.ts +1671 -0
- package/src/application/BootDoctor.ts +108 -0
- package/src/application/DevErrorPage.ts +567 -0
- package/src/application/ExceptionHandler.ts +183 -0
- package/src/application/currentApp.ts +73 -0
- package/src/assets/assets.ts +79 -0
- package/src/assets/index.ts +16 -0
- package/src/auth/AuthenticatedUser.ts +18 -0
- package/src/build/PackageLinter.ts +146 -0
- package/src/build/PackageScaffold.ts +127 -0
- package/src/build/codemod.ts +64 -0
- package/src/build/index.ts +12 -0
- package/src/command/Command.ts +254 -0
- package/src/command/CommandRunner.ts +593 -0
- package/src/command/OutputWriter.ts +61 -0
- package/src/command/builtin/CompileCommand.ts +46 -0
- package/src/command/builtin/CssBuildCommand.ts +71 -0
- package/src/command/builtin/KeyGenerateCommand.ts +58 -0
- package/src/command/builtin/LintPackagesCommand.ts +72 -0
- package/src/command/builtin/MakeCommandCommand.ts +85 -0
- package/src/command/builtin/MakeControllerCommand.ts +95 -0
- package/src/command/builtin/MakeEventCommand.ts +85 -0
- package/src/command/builtin/MakeJobCommand.ts +53 -0
- package/src/command/builtin/MakeListenerCommand.ts +35 -0
- package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
- package/src/command/builtin/MakeNotificationCommand.ts +48 -0
- package/src/command/builtin/MakeObserverCommand.ts +78 -0
- package/src/command/builtin/MakePackageCommand.ts +45 -0
- package/src/command/builtin/MakePolicyCommand.ts +66 -0
- package/src/command/builtin/MakeProviderCommand.ts +75 -0
- package/src/command/builtin/MakeRequestCommand.ts +47 -0
- package/src/command/builtin/MakeResourceCommand.ts +61 -0
- package/src/command/builtin/MakeTestCommand.ts +120 -0
- package/src/command/builtin/ReloadCommand.ts +52 -0
- package/src/command/builtin/ReplCommand.ts +174 -0
- package/src/command/builtin/RouteListCommand.ts +188 -0
- package/src/command/builtin/ServeCommand.ts +321 -0
- package/src/command/builtin/StartCommand.ts +3 -0
- package/src/command/builtin/StatusCommand.ts +71 -0
- package/src/command/builtin/TestCommand.ts +172 -0
- package/src/command/builtin/WorkerCommand.ts +27 -0
- package/src/command/builtin/index.ts +53 -0
- package/src/command/scaffold/worker.ts.txt +12 -0
- package/src/command/scaffold/zerotal.ts.txt +26 -0
- package/src/command/startZerotal.ts +55 -0
- package/src/config/AppConfig.ts +253 -0
- package/src/config/ConfigLoader.ts +117 -0
- package/src/config/ConfigManager.ts +169 -0
- package/src/config/index.ts +46 -0
- package/src/config/registry.ts +59 -0
- package/src/config/validation.ts +117 -0
- package/src/container/Container.ts +606 -0
- package/src/container/ContextualBindingBuilder.ts +57 -0
- package/src/container/ScopedResolver.ts +117 -0
- package/src/container/index.ts +32 -0
- package/src/container/inject.ts +55 -0
- package/src/container/types.ts +71 -0
- package/src/context/RequestContext.ts +91 -0
- package/src/contracts/auth.ts +24 -0
- package/src/contracts/index.ts +23 -0
- package/src/contracts/session.ts +70 -0
- package/src/contracts/transaction.ts +26 -0
- package/src/conventions/ConventionLoader.ts +128 -0
- package/src/conventions/builtinConcerns.ts +131 -0
- package/src/crypt/Crypt.ts +141 -0
- package/src/crypt/URLSigner.ts +96 -0
- package/src/datetime/Carbon.ts +1396 -0
- package/src/datetime/CarbonInterval.ts +421 -0
- package/src/datetime/clock.ts +28 -0
- package/src/datetime/index.ts +23 -0
- package/src/datetime/temporal-shim.ts +1 -0
- package/src/dev/BuildOutput.ts +131 -0
- package/src/dev/CssPlugins.ts +184 -0
- package/src/dev/DevBuildHook.ts +74 -0
- package/src/dev/DevOrchestrator.ts +213 -0
- package/src/dev/DevReloadMiddleware.ts +101 -0
- package/src/dev/DevReloadServer.ts +85 -0
- package/src/dev/DevWsServer.ts +45 -0
- package/src/dev/index.ts +19 -0
- package/src/dev/reloadClient.ts +39 -0
- package/src/env/Def.ts +232 -0
- package/src/env/EnvSchema.ts +105 -0
- package/src/env/index.ts +34 -0
- package/src/env/t.ts +128 -0
- package/src/errors/ConfigError.ts +12 -0
- package/src/errors/ContainerErrors.ts +143 -0
- package/src/errors/HttpError.ts +127 -0
- package/src/errors/ValidationError.ts +19 -0
- package/src/errors/ZerotalError.ts +25 -0
- package/src/errors/index.ts +46 -0
- package/src/events/CallQueuedListener.ts +66 -0
- package/src/events/Emitter.ts +280 -0
- package/src/events/EventFake.ts +160 -0
- package/src/events/FrameworkEvents.ts +252 -0
- package/src/facade/Facade.ts +101 -0
- package/src/facade/facades/App.ts +155 -0
- package/src/facade/facades/Artisan.ts +63 -0
- package/src/facade/facades/Config.ts +21 -0
- package/src/facade/facades/Events.ts +19 -0
- package/src/facade/facades/index.ts +28 -0
- package/src/global.d.ts +9 -0
- package/src/hash/Hash.ts +60 -0
- package/src/health/Health.ts +221 -0
- package/src/health/index.ts +27 -0
- package/src/helpers/Collection.ts +435 -0
- package/src/helpers/config.ts +59 -0
- package/src/helpers/fluent.ts +52 -0
- package/src/helpers/html.ts +11 -0
- package/src/helpers/index.ts +266 -0
- package/src/helpers/make.ts +35 -0
- package/src/helpers/markdown.ts +73 -0
- package/src/helpers/pageElements.ts +27 -0
- package/src/helpers/request.ts +62 -0
- package/src/helpers/response.ts +411 -0
- package/src/helpers/str.ts +208 -0
- package/src/http/Http.ts +298 -0
- package/src/http/HttpClient.ts +289 -0
- package/src/http/Resource.ts +171 -0
- package/src/http/UploadedFile.ts +204 -0
- package/src/http/Uri.ts +490 -0
- package/src/http/index.ts +46 -0
- package/src/http/negotiate.ts +213 -0
- package/src/http/originGuard.ts +76 -0
- package/src/http/sniffContentType.ts +105 -0
- package/src/http/url.ts +204 -0
- package/src/http/withHeaders.ts +24 -0
- package/src/index.ts +250 -0
- package/src/lock/LockManager.ts +228 -0
- package/src/lock/config.ts +49 -0
- package/src/lock/drivers/LockDriver.ts +32 -0
- package/src/lock/drivers/MemoryLockDriver.ts +52 -0
- package/src/lock/drivers/RedisLockDriver.ts +58 -0
- package/src/lock/drivers/SqliteLockDriver.ts +85 -0
- package/src/lock/errors.ts +20 -0
- package/src/lock/facades/Lock.ts +114 -0
- package/src/lock/index.ts +53 -0
- package/src/logger/Log.ts +35 -0
- package/src/logger/LogManager.ts +430 -0
- package/src/logger/LoggerMiddleware.ts +125 -0
- package/src/logger/channels/ConsoleChannel.ts +139 -0
- package/src/logger/channels/DailyChannel.ts +74 -0
- package/src/logger/channels/NullChannel.ts +17 -0
- package/src/logger/channels/SingleChannel.ts +34 -0
- package/src/logger/channels/StackChannel.ts +29 -0
- package/src/logger/config.ts +90 -0
- package/src/logger/format.ts +96 -0
- package/src/logger/frameworkLog.ts +93 -0
- package/src/logger/index.ts +68 -0
- package/src/logger/renderTable.ts +111 -0
- package/src/logger/types.ts +212 -0
- package/src/macros/config.macro.ts +50 -0
- package/src/metrics/HttpMetrics.ts +114 -0
- package/src/metrics/index.ts +18 -0
- package/src/middleware/BaseMiddleware.ts +72 -0
- package/src/middleware/CorsMiddleware.ts +152 -0
- package/src/middleware/RateLimiter.ts +255 -0
- package/src/middleware/SecureHeadersMiddleware.ts +127 -0
- package/src/middleware/ThrottleMiddleware.ts +252 -0
- package/src/middleware/WebhookMiddleware.ts +204 -0
- package/src/pipeline/ContextRegistry.ts +42 -0
- package/src/pipeline/HttpContext.ts +865 -0
- package/src/pipeline/Pipeline.ts +150 -0
- package/src/pipeline/currentPage.ts +46 -0
- package/src/pipeline/types.ts +80 -0
- package/src/provider/LockProvider.ts +64 -0
- package/src/provider/LogProvider.ts +137 -0
- package/src/provider/ServiceProvider.ts +84 -0
- package/src/provider/StorageProvider.ts +45 -0
- package/src/router/FileRouter.ts +526 -0
- package/src/router/Route.ts +76 -0
- package/src/router/RouteHandler.ts +335 -0
- package/src/router/Router.ts +1247 -0
- package/src/router/domain.ts +65 -0
- package/src/security/index.ts +22 -0
- package/src/storage/FakeDisk.ts +233 -0
- package/src/storage/StorageFilesMiddleware.ts +150 -0
- package/src/storage/StorageManager.ts +173 -0
- package/src/storage/config.ts +47 -0
- package/src/storage/drivers/LocalDriver.ts +138 -0
- package/src/storage/drivers/S3Driver.ts +169 -0
- package/src/storage/errors.ts +135 -0
- package/src/storage/facades/Storage.ts +3 -0
- package/src/storage/global.d.ts +7 -0
- package/src/storage/index.ts +22 -0
- package/src/storage/root.ts +59 -0
- package/src/storage/types.ts +104 -0
- package/src/support/appKey.ts +38 -0
- package/src/support/cookie.ts +72 -0
- package/src/support/crypto.ts +52 -0
- package/src/support/deepMerge.ts +117 -0
- package/src/support/env.ts +71 -0
- package/src/support/network.ts +79 -0
- package/src/support/port.ts +197 -0
- package/src/support/str.ts +122 -0
- package/src/view/FileRouteResolver.ts +59 -0
- package/src/view/index.ts +144 -0
- package/src/view/jsx-runtime.ts +233 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `make:observer` command, which scaffolds a model observer class.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "../Command.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `bun zt make:observer <name>` — scaffolds a new model observer class under
|
|
8
|
+
* `app/observers/` (pass `--model`/`-m` to target a specific model).
|
|
9
|
+
*
|
|
10
|
+
* @category Scaffolding (make:*)
|
|
11
|
+
*/
|
|
12
|
+
export class MakeObserverCommand extends Command {
|
|
13
|
+
static commandName = "make:observer";
|
|
14
|
+
static description = "Create a new model observer class";
|
|
15
|
+
static needsApp = false;
|
|
16
|
+
|
|
17
|
+
static args = [
|
|
18
|
+
{ name: "name", required: true, description: "Observer name (e.g. UserObserver)" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
static flags = [
|
|
22
|
+
{
|
|
23
|
+
name: "model",
|
|
24
|
+
short: "m",
|
|
25
|
+
type: "string" as const,
|
|
26
|
+
description: "Model the observer is for (e.g. User)",
|
|
27
|
+
default: "",
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
async run(): Promise<void> {
|
|
32
|
+
const name = this.args["name"]!;
|
|
33
|
+
const model = (this.flags["model"] as string) || name.replace(/Observer$/, "");
|
|
34
|
+
const path = `app/observers/${name}.ts`;
|
|
35
|
+
|
|
36
|
+
if (await Bun.file(path).exists()) {
|
|
37
|
+
this.error(`File already exists: ${path}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await Bun.write(path, _stub(name, model));
|
|
42
|
+
this.info(`Created: ${path}`);
|
|
43
|
+
this.line("");
|
|
44
|
+
this.line(`Register it in a ServiceProvider:`);
|
|
45
|
+
this.dim(` ${model}.observe(${name});`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _stub(name: string, model: string): string {
|
|
50
|
+
return `import type { ModelObserver } from '@zerotal/orm';
|
|
51
|
+
|
|
52
|
+
export class ${name} implements ModelObserver {
|
|
53
|
+
creating(${model.toLowerCase()}: Record<string, unknown>): void {
|
|
54
|
+
//
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
created(${model.toLowerCase()}: Record<string, unknown>): void {
|
|
58
|
+
//
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
updating(${model.toLowerCase()}: Record<string, unknown>): void {
|
|
62
|
+
//
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
updated(${model.toLowerCase()}: Record<string, unknown>): void {
|
|
66
|
+
//
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
deleting(${model.toLowerCase()}: Record<string, unknown>): void {
|
|
70
|
+
//
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
deleted(${model.toLowerCase()}: Record<string, unknown>): void {
|
|
74
|
+
//
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `make:package` command, which scaffolds a conformant `@zerotal/<name>`
|
|
3
|
+
* workspace package on disk.
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from "../Command.ts";
|
|
6
|
+
import { scaffoldPackage, packageNames } from "../../build/PackageScaffold.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `bun zt make:package <name>` — scaffolds a conformant `@zerotal/<name>`
|
|
10
|
+
* workspace package on disk.
|
|
11
|
+
*
|
|
12
|
+
* @category Scaffolding (make:*)
|
|
13
|
+
*/
|
|
14
|
+
export class MakePackageCommand extends Command {
|
|
15
|
+
static commandName = "make:package";
|
|
16
|
+
static description = "Scaffold a conformant @zerotal/<name> package";
|
|
17
|
+
static needsApp = false;
|
|
18
|
+
static args = [
|
|
19
|
+
{ name: "name", required: true },
|
|
20
|
+
{ name: "baseDir", required: false, default: "./packages" },
|
|
21
|
+
];
|
|
22
|
+
static flags = [];
|
|
23
|
+
|
|
24
|
+
async run(): Promise<void> {
|
|
25
|
+
const rawName = this.args["name"]!;
|
|
26
|
+
const baseDir = (this.args["baseDir"] as string | undefined) || "./packages";
|
|
27
|
+
const { token } = packageNames(rawName);
|
|
28
|
+
if (!token) {
|
|
29
|
+
this.error(`Invalid package name: "${rawName}"`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const packageDir = `${baseDir}/${token}`;
|
|
33
|
+
if (await Bun.file(`${packageDir}/package.json`).exists()) {
|
|
34
|
+
this.error(`Package already exists: ${packageDir}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const files = scaffoldPackage(rawName);
|
|
38
|
+
for (const [relativePath, content] of files)
|
|
39
|
+
await Bun.write(`${packageDir}/${relativePath}`, content);
|
|
40
|
+
this.section(`Created @zerotal/${token}`);
|
|
41
|
+
for (const relativePath of files.keys()) this.info(` ${packageDir}/${relativePath}`);
|
|
42
|
+
this.newLine();
|
|
43
|
+
this.line(`Next: add it to your app's providers, then run 'bun zerotal lint:packages'.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `make:policy` command and the authorization-policy source stub it writes.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "../Command.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `bun zt make:policy <name>` — scaffolds a new authorization policy class
|
|
8
|
+
* under `app/policies/` (pass `--model`/`-m` to bind it to a model).
|
|
9
|
+
*
|
|
10
|
+
* @category Scaffolding (make:*)
|
|
11
|
+
*/
|
|
12
|
+
export class MakePolicyCommand extends Command {
|
|
13
|
+
static commandName = "make:policy";
|
|
14
|
+
static description = "Create a new authorization policy class";
|
|
15
|
+
static needsApp = false;
|
|
16
|
+
static args = [{ name: "name", required: true, description: "Policy name (e.g. PostPolicy)" }];
|
|
17
|
+
static flags = [
|
|
18
|
+
{
|
|
19
|
+
name: "model",
|
|
20
|
+
short: "m",
|
|
21
|
+
type: "string" as const,
|
|
22
|
+
description: "The model this policy is for (e.g. Post)",
|
|
23
|
+
default: "",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
async run(): Promise<void> {
|
|
28
|
+
const name = this.args["name"]!;
|
|
29
|
+
const model = (this.flags["model"] as string) || name.replace(/Policy$/, "");
|
|
30
|
+
const path = `app/policies/${name}.ts`;
|
|
31
|
+
|
|
32
|
+
if (await Bun.file(path).exists()) {
|
|
33
|
+
this.error(`File already exists: ${path}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await Bun.write(path, policyStub(name, model));
|
|
38
|
+
this.info(`Created: ${path}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Source for a new authorization policy class extending `Policy`. */
|
|
43
|
+
export function policyStub(name: string, model: string): string {
|
|
44
|
+
return `import { Policy } from '@zerotal/auth';
|
|
45
|
+
// import type { ${model} } from '../models/${model}.ts';
|
|
46
|
+
// import type { User } from '../models/User.ts';
|
|
47
|
+
|
|
48
|
+
export class ${name} extends Policy<unknown> {
|
|
49
|
+
view(_user: unknown | null, _resource: unknown): boolean {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
create(_user: unknown): boolean {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
update(_user: unknown, _resource: unknown): boolean {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
delete(_user: unknown, _resource: unknown): boolean {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `make:provider` command, which scaffolds a service provider and (unless
|
|
3
|
+
* skipped) wires it into `bootstrap/providers.ts`.
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from "../Command.ts";
|
|
6
|
+
import { registerProvider } from "../../build/codemod.ts";
|
|
7
|
+
|
|
8
|
+
function providerStub(name: string): string {
|
|
9
|
+
return `import { ServiceProvider } from '@zerotal/core';
|
|
10
|
+
|
|
11
|
+
export class ${name} extends ServiceProvider {
|
|
12
|
+
override onRegister(): void {
|
|
13
|
+
// Bind singletons into the container here.
|
|
14
|
+
// this.app.container.singleton('my-service', () => new MyService());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override async onBooting(): Promise<void> {
|
|
18
|
+
// Called after all providers are registered.
|
|
19
|
+
// Safe to resolve other bindings from the container.
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override async onBooted(): Promise<void> {
|
|
23
|
+
// Called after all providers have finished booting.
|
|
24
|
+
// Use for cross-provider wiring (e.g. registering commands, events).
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `bun zt make:provider <name>` — scaffolds a new service provider class under
|
|
32
|
+
* `app/providers/` and (unless skipped) wires it into `bootstrap/providers.ts`.
|
|
33
|
+
*
|
|
34
|
+
* @category Scaffolding (make:*)
|
|
35
|
+
*/
|
|
36
|
+
export class MakeProviderCommand extends Command {
|
|
37
|
+
static commandName = "make:provider";
|
|
38
|
+
static description = "Create a new service provider class";
|
|
39
|
+
static needsApp = false;
|
|
40
|
+
static args = [{ name: "name", required: true }];
|
|
41
|
+
static flags = [
|
|
42
|
+
{
|
|
43
|
+
name: "no-register",
|
|
44
|
+
type: "boolean" as const,
|
|
45
|
+
description: "Skip registering in bootstrap/providers.ts",
|
|
46
|
+
default: false,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
async run(): Promise<void> {
|
|
51
|
+
const name = this.args["name"]!;
|
|
52
|
+
const className = name.endsWith("Provider") ? name : `${name}Provider`;
|
|
53
|
+
const path = `app/providers/${className}.ts`;
|
|
54
|
+
|
|
55
|
+
if (await Bun.file(path).exists()) {
|
|
56
|
+
this.error(`File already exists: ${path}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await Bun.write(path, providerStub(className));
|
|
61
|
+
this.info(`Created: ${path}`);
|
|
62
|
+
|
|
63
|
+
// Codemod: wire the new provider into bootstrap/providers.ts.
|
|
64
|
+
if (this.flags["no-register"] !== true) {
|
|
65
|
+
const result = await registerProvider({
|
|
66
|
+
className,
|
|
67
|
+
importPath: `../app/providers/${className}.ts`,
|
|
68
|
+
});
|
|
69
|
+
if (result === "added") this.info("Registered in bootstrap/providers.ts");
|
|
70
|
+
if (result === "exists") this.info("Already registered in bootstrap/providers.ts");
|
|
71
|
+
if (result === "missing")
|
|
72
|
+
this.warn("bootstrap/providers.ts not found — add the provider manually.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `make:request` command, which scaffolds a FormRequest validation class.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "../Command.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `bun zt make:request <name>` — scaffolds a new FormRequest validation class
|
|
8
|
+
* under `app/requests/`.
|
|
9
|
+
*
|
|
10
|
+
* @category Scaffolding (make:*)
|
|
11
|
+
*/
|
|
12
|
+
export class MakeRequestCommand extends Command {
|
|
13
|
+
static commandName = "make:request";
|
|
14
|
+
static description = "Create a new FormRequest class";
|
|
15
|
+
static needsApp = false;
|
|
16
|
+
static override args = [{ name: "name", required: true, default: "" }];
|
|
17
|
+
|
|
18
|
+
async run(): Promise<void> {
|
|
19
|
+
const name = this.args["name"];
|
|
20
|
+
if (!name) {
|
|
21
|
+
this.error("Name is required.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const path = `app/requests/${name}.ts`;
|
|
25
|
+
if (await Bun.file(path).exists()) {
|
|
26
|
+
this.error(`File already exists: ${path}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await Bun.write(path, stub(name));
|
|
30
|
+
this.info(`Created: ${path}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stub(name: string): string {
|
|
35
|
+
return `import { FormRequest } from '@zerotal/validator';
|
|
36
|
+
import type { RuleBuilder } from '@zerotal/validator';
|
|
37
|
+
import type { FieldRule } from '@zerotal/validator';
|
|
38
|
+
|
|
39
|
+
export class ${name} extends FormRequest {
|
|
40
|
+
rules(r: RuleBuilder): Record<string, FieldRule> {
|
|
41
|
+
return {
|
|
42
|
+
// example: title: r.string().min(3).max(255),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `make:resource` command, which scaffolds an API resource transformer.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "../Command.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `bun zt make:resource <name>` — scaffolds a new API resource transformer
|
|
8
|
+
* class under `app/resources/`.
|
|
9
|
+
*
|
|
10
|
+
* @category Scaffolding (make:*)
|
|
11
|
+
*/
|
|
12
|
+
export class MakeResourceCommand extends Command {
|
|
13
|
+
static commandName = "make:resource";
|
|
14
|
+
static description = "Create a new API resource transformer class";
|
|
15
|
+
static needsApp = false;
|
|
16
|
+
|
|
17
|
+
static args = [
|
|
18
|
+
{ name: "name", required: true, description: "Resource name (e.g. UserResource)" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
async run(): Promise<void> {
|
|
22
|
+
const name = this.args["name"]!;
|
|
23
|
+
const path = `app/resources/${name}.ts`;
|
|
24
|
+
|
|
25
|
+
if (await Bun.file(path).exists()) {
|
|
26
|
+
this.error(`File already exists: ${path}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await Bun.write(path, _stub(name));
|
|
31
|
+
this.info(`Created: ${path}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _stub(name: string): string {
|
|
36
|
+
const model = name.replace(/Resource$/, "");
|
|
37
|
+
const snake = model.replace(/([A-Z])/g, (char, index) => (index ? "-" : "") + char.toLowerCase());
|
|
38
|
+
return `import { Resource, ResourceCollection } from '@zerotal/core';
|
|
39
|
+
import type { PaginatedData } from '@zerotal/core';
|
|
40
|
+
|
|
41
|
+
export class ${name} extends Resource<${model}> {
|
|
42
|
+
toArray(): Record<string, unknown> {
|
|
43
|
+
return {
|
|
44
|
+
id: this.resource.id,
|
|
45
|
+
// Add fields here — omit sensitive ones like 'password'
|
|
46
|
+
created_at: this.resource.createdAt,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Usage in controllers ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
// Single model:
|
|
54
|
+
// ctx.json(new ${name}(model).toJson()); // { data: { ... } }
|
|
55
|
+
// ctx.response = new ${name}(model).toResponse(201);
|
|
56
|
+
|
|
57
|
+
// Paginated list (ResourceCollection adds { data, meta, links } automatically):
|
|
58
|
+
// const paginated = await ${model}.query().paginate(15, page);
|
|
59
|
+
// ctx.json(ResourceCollection.of(${name}, paginated, '/api/${snake}'));
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `make:test` command and the test stubs it writes.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "../Command.ts";
|
|
5
|
+
import { pluralize } from "../../support/str.ts";
|
|
6
|
+
|
|
7
|
+
/** A feature test: boots the app and drives it over real HTTP. */
|
|
8
|
+
export function featureTestStub(name: string): string {
|
|
9
|
+
const subject = name.replace(/Test$/, "");
|
|
10
|
+
// Resource routes and tables are plural, so the stub is a starting point you
|
|
11
|
+
// edit rather than one you have to rewrite.
|
|
12
|
+
const resource = pluralize(subject.toLowerCase());
|
|
13
|
+
return `import { describe, it, beforeAll, afterAll } from 'bun:test';
|
|
14
|
+
import { migrateDatabase, refreshDatabase, assertDatabaseHas } from '@zerotal/testing';
|
|
15
|
+
import type { TestApp } from '@zerotal/testing';
|
|
16
|
+
import { createApp } from '../helpers.ts';
|
|
17
|
+
|
|
18
|
+
let app: TestApp;
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
app = await createApp();
|
|
22
|
+
await migrateDatabase();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(() => app.close());
|
|
26
|
+
|
|
27
|
+
describe('${subject}', () => {
|
|
28
|
+
// Each test runs in a transaction that rolls back, so they start from the
|
|
29
|
+
// same clean database and can be read in any order.
|
|
30
|
+
refreshDatabase();
|
|
31
|
+
|
|
32
|
+
it('lists ${resource}', async () => {
|
|
33
|
+
const res = await app.get('/${resource}');
|
|
34
|
+
|
|
35
|
+
res.assertOk();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rejects an invalid payload', async () => {
|
|
39
|
+
const res = await app.asJson().post('/${resource}', {});
|
|
40
|
+
|
|
41
|
+
res.assertUnprocessable();
|
|
42
|
+
res.assertInvalid();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('creates a record', async () => {
|
|
46
|
+
const res = await app.asJson().post('/${resource}', { name: 'Example' });
|
|
47
|
+
|
|
48
|
+
res.assertCreated();
|
|
49
|
+
await assertDatabaseHas('${resource}', { name: 'Example' });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A unit test: no server, no database, just the thing under test. */
|
|
56
|
+
export function unitTestStub(name: string): string {
|
|
57
|
+
const subject = name.replace(/Test$/, "");
|
|
58
|
+
return `import { describe, it, expect } from 'bun:test';
|
|
59
|
+
|
|
60
|
+
describe('${subject}', () => {
|
|
61
|
+
it('does the thing it exists to do', () => {
|
|
62
|
+
expect(true).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function testPath(name: string, unit: boolean): string {
|
|
69
|
+
const file = name.endsWith("Test") ? name : `${name}Test`;
|
|
70
|
+
return `tests/${unit ? "unit" : "feature"}/${file}.ts`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `bun zt make:test <name>` — scaffolds a test file under `tests/feature/`
|
|
75
|
+
* (or `tests/unit/` with `--unit`).
|
|
76
|
+
*
|
|
77
|
+
* The feature stub boots the app through `tests/helpers.ts`, the file the
|
|
78
|
+
* templates scaffold, so a generated test runs against the same configured
|
|
79
|
+
* application as every other test in the suite rather than building its own.
|
|
80
|
+
*
|
|
81
|
+
* @category Scaffolding (make:*)
|
|
82
|
+
*/
|
|
83
|
+
export class MakeTestCommand extends Command {
|
|
84
|
+
static commandName = "make:test";
|
|
85
|
+
static description = "Create a new test file";
|
|
86
|
+
static needsApp = false;
|
|
87
|
+
|
|
88
|
+
static get args() {
|
|
89
|
+
return [{ name: "name", required: true, description: "Test name (e.g. PostTest)" }];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static get flags() {
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
name: "unit",
|
|
96
|
+
type: "boolean" as const,
|
|
97
|
+
description: "Create a unit test (no app, no database) instead of a feature test",
|
|
98
|
+
default: false,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async run(): Promise<void> {
|
|
104
|
+
const name = this.args["name"]!;
|
|
105
|
+
const unit = this.flags["unit"] === true;
|
|
106
|
+
const path = testPath(name, unit);
|
|
107
|
+
|
|
108
|
+
if (await Bun.file(path).exists()) {
|
|
109
|
+
this.error(`File already exists: ${path}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Bun.write() creates any missing parent directories, so no mkdir is needed.
|
|
114
|
+
await Bun.write(path, unit ? unitTestStub(name) : featureTestStub(name));
|
|
115
|
+
this.info(`Created: ${path}`);
|
|
116
|
+
if (!unit) {
|
|
117
|
+
this.dim("Feature tests boot the app through tests/helpers.ts — create it if you have not.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { Command } from "../Command.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `bun zt reload` — hot-reloads routes in the running server.
|
|
6
|
+
*
|
|
7
|
+
* Sends SIGUSR2 to the running Zerotal server, which triggers a zero-downtime
|
|
8
|
+
* route hot-swap via `Bun.serve().reload()`. In-flight requests are not
|
|
9
|
+
* dropped — they finish on the old route table while new requests immediately
|
|
10
|
+
* use the updated routes.
|
|
11
|
+
*
|
|
12
|
+
* Requires: a server started with `bun zt serve` (writes .zerotal/server.pid).
|
|
13
|
+
*
|
|
14
|
+
* Platform note: SIGUSR2 is a Unix/macOS signal. On Windows, use WSL or send
|
|
15
|
+
* the signal via another mechanism.
|
|
16
|
+
*
|
|
17
|
+
* @category Serving
|
|
18
|
+
*/
|
|
19
|
+
export class ReloadCommand extends Command {
|
|
20
|
+
static commandName = "reload";
|
|
21
|
+
static description = "Hot-reload routes in the running server (sends SIGUSR2)";
|
|
22
|
+
static needsApp = false;
|
|
23
|
+
static args = [];
|
|
24
|
+
static flags = [];
|
|
25
|
+
|
|
26
|
+
async run(): Promise<void> {
|
|
27
|
+
const pidFile = join(process.cwd(), ".zerotal", "server.pid");
|
|
28
|
+
|
|
29
|
+
let rawPid: string;
|
|
30
|
+
try {
|
|
31
|
+
rawPid = await Bun.file(pidFile).text();
|
|
32
|
+
} catch {
|
|
33
|
+
this.error("No .zerotal/server.pid found. Is the server running?");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const pid = parseInt(rawPid.trim(), 10);
|
|
38
|
+
if (!pid || isNaN(pid)) {
|
|
39
|
+
this.error(`PID file is invalid: "${rawPid.trim()}"`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid, "SIGUSR2");
|
|
45
|
+
this.info(`↺ SIGUSR2 sent to PID ${pid} — routes are reloading.`);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.error(`Failed to signal process ${pid}: ${(error as Error).message}`);
|
|
48
|
+
this.dim("Is the server still running? Check with: bun zerotal.ts status");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|