@malyuga/aether-cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +119 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/check.d.ts +4 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +45 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +62 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/generate.d.ts +15 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +51 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/help.d.ts +6 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +60 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/upgrade.d.ts +5 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/core/ast.d.ts +7 -0
- package/dist/core/ast.d.ts.map +1 -0
- package/dist/core/ast.js +90 -0
- package/dist/core/ast.js.map +1 -0
- package/dist/core/constants.d.ts +133 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +135 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/diff.d.ts +20 -0
- package/dist/core/diff.d.ts.map +1 -0
- package/dist/core/diff.js +39 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/errors.d.ts +19 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +37 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/naming.d.ts +10 -0
- package/dist/core/naming.d.ts.map +1 -0
- package/dist/core/naming.js +50 -0
- package/dist/core/naming.js.map +1 -0
- package/dist/core/paths.d.ts +10 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +38 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/plan.d.ts +37 -0
- package/dist/core/plan.d.ts.map +1 -0
- package/dist/core/plan.js +126 -0
- package/dist/core/plan.js.map +1 -0
- package/dist/core/project.d.ts +16 -0
- package/dist/core/project.d.ts.map +1 -0
- package/dist/core/project.js +66 -0
- package/dist/core/project.js.map +1 -0
- package/dist/core/render.d.ts +4 -0
- package/dist/core/render.d.ts.map +1 -0
- package/dist/core/render.js +44 -0
- package/dist/core/render.js.map +1 -0
- package/dist/generators/middleware.d.ts +11 -0
- package/dist/generators/middleware.d.ts.map +1 -0
- package/dist/generators/middleware.js +41 -0
- package/dist/generators/middleware.js.map +1 -0
- package/dist/generators/openapi.d.ts +9 -0
- package/dist/generators/openapi.d.ts.map +1 -0
- package/dist/generators/openapi.js +46 -0
- package/dist/generators/openapi.js.map +1 -0
- package/dist/generators/project.d.ts +9 -0
- package/dist/generators/project.d.ts.map +1 -0
- package/dist/generators/project.js +76 -0
- package/dist/generators/project.js.map +1 -0
- package/dist/generators/resource-context.d.ts +18 -0
- package/dist/generators/resource-context.d.ts.map +1 -0
- package/dist/generators/resource-context.js +21 -0
- package/dist/generators/resource-context.js.map +1 -0
- package/dist/generators/resource-file.d.ts +8 -0
- package/dist/generators/resource-file.d.ts.map +1 -0
- package/dist/generators/resource-file.js +9 -0
- package/dist/generators/resource-file.js.map +1 -0
- package/dist/generators/resource-tests.d.ts +5 -0
- package/dist/generators/resource-tests.d.ts.map +1 -0
- package/dist/generators/resource-tests.js +17 -0
- package/dist/generators/resource-tests.js.map +1 -0
- package/dist/generators/resource.d.ts +21 -0
- package/dist/generators/resource.d.ts.map +1 -0
- package/dist/generators/resource.js +64 -0
- package/dist/generators/resource.js.map +1 -0
- package/dist/generators/types.d.ts +7 -0
- package/dist/generators/types.d.ts.map +1 -0
- package/dist/generators/types.js +2 -0
- package/dist/generators/types.js.map +1 -0
- package/dist/help/guide.txt +126 -0
- package/dist/openapi/extract-resources.d.ts +3 -0
- package/dist/openapi/extract-resources.d.ts.map +1 -0
- package/dist/openapi/extract-resources.js +130 -0
- package/dist/openapi/extract-resources.js.map +1 -0
- package/dist/openapi/load-spec.d.ts +3 -0
- package/dist/openapi/load-spec.d.ts.map +1 -0
- package/dist/openapi/load-spec.js +19 -0
- package/dist/openapi/load-spec.js.map +1 -0
- package/dist/openapi/types.d.ts +51 -0
- package/dist/openapi/types.d.ts.map +1 -0
- package/dist/openapi/types.js +2 -0
- package/dist/openapi/types.js.map +1 -0
- package/dist/templates/init/aether.config.json.eta +5 -0
- package/dist/templates/init/astraeus.config.json.eta +5 -0
- package/dist/templates/init/chisel.config.json.eta +5 -0
- package/dist/templates/init/env.eta +8 -0
- package/dist/templates/init/env.example.eta +8 -0
- package/dist/templates/init/gitignore.eta +5 -0
- package/dist/templates/init/package.json.eta +14 -0
- package/dist/templates/init/src-app-composition.ts.eta +6 -0
- package/dist/templates/init/src-app-config.ts.eta +105 -0
- package/dist/templates/init/src-app-http.ts.eta +74 -0
- package/dist/templates/init/src-app-list.ts.eta +35 -0
- package/dist/templates/init/src-app-logger.ts.eta +35 -0
- package/dist/templates/init/src-app-middleware-bearer-auth.ts.eta +60 -0
- package/dist/templates/init/src-app-middleware-body-limit.ts.eta +37 -0
- package/dist/templates/init/src-app-middleware-cors.ts.eta +53 -0
- package/dist/templates/init/src-app-middleware-error-handler.ts.eta +15 -0
- package/dist/templates/init/src-app-middleware-request-logger.ts.eta +8 -0
- package/dist/templates/init/src-app-middleware-types.ts.eta +7 -0
- package/dist/templates/init/src-app-router.ts.eta +72 -0
- package/dist/templates/init/src-app-server.ts.eta +16 -0
- package/dist/templates/init/src-app.ts.eta +36 -0
- package/dist/templates/init/src-health-health.routes.ts.eta +12 -0
- package/dist/templates/init/src-main.ts.eta +37 -0
- package/dist/templates/init/tsconfig.json.eta +14 -0
- package/dist/templates/middleware/middleware.ts.eta +6 -0
- package/dist/templates/resource/controller.ts.eta +135 -0
- package/dist/templates/resource/module.test.ts.eta +16 -0
- package/dist/templates/resource/module.ts.eta +25 -0
- package/dist/templates/resource/repository.fake.ts.eta +33 -0
- package/dist/templates/resource/repository.ts.eta +14 -0
- package/dist/templates/resource/routes.ts.eta +18 -0
- package/dist/templates/resource/service.ts.eta +45 -0
- package/dist/templates/resource/types.ts.eta +12 -0
- package/dist/templates/resource/validate.ts.eta +64 -0
- package/llms.txt +57 -0
- package/package.json +67 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"types": ["node"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { Params } from "../app/router";
|
|
3
|
+
import { JsonBodyError, readJson, sendError, sendJson, HTTP_STATUS } from "../app/http";
|
|
4
|
+
import type { <%= it.entityPascal %>Service } from "./<%= it.resourceKebab %>.service";
|
|
5
|
+
<% if (it.crud) { %>
|
|
6
|
+
import { parseCreateInput, parseUpdateInput } from "./<%= it.resourceKebab %>.validate";
|
|
7
|
+
import { parseListQuery } from "../app/list";
|
|
8
|
+
<% } %>
|
|
9
|
+
|
|
10
|
+
const REPOSITORY_NOT_CONFIGURED = "Repository not configured";
|
|
11
|
+
|
|
12
|
+
<% if (it.crud) { %>
|
|
13
|
+
async function parseJson(req: IncomingMessage): Promise<unknown> {
|
|
14
|
+
try {
|
|
15
|
+
return await readJson(req);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (err instanceof JsonBodyError) {
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
throw new JsonBodyError("Invalid JSON body");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function create<%= it.entityPascal %>Controller(deps: {
|
|
25
|
+
service: <%= it.entityPascal %>Service | null;
|
|
26
|
+
repositoryConfigured: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
return {
|
|
29
|
+
async list(req: IncomingMessage, res: ServerResponse, _params: Params): Promise<void> {
|
|
30
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
31
|
+
const parsed = parseListQuery(url.searchParams);
|
|
32
|
+
if (!parsed.ok) {
|
|
33
|
+
sendError(res, HTTP_STATUS.BAD_REQUEST, parsed.message);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!deps.repositoryConfigured || !deps.service) {
|
|
37
|
+
sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
sendJson(res, HTTP_STATUS.OK, deps.service.list(parsed.value));
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async getById(_req: IncomingMessage, res: ServerResponse, params: Params): Promise<void> {
|
|
44
|
+
if (!deps.repositoryConfigured || !deps.service) {
|
|
45
|
+
sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const item = deps.service.getById(params.id!);
|
|
49
|
+
if (!item) {
|
|
50
|
+
sendError(res, HTTP_STATUS.NOT_FOUND, "Not found");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
sendJson(res, HTTP_STATUS.OK, item);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async create(req: IncomingMessage, res: ServerResponse, _params: Params): Promise<void> {
|
|
57
|
+
if (!deps.repositoryConfigured || !deps.service) {
|
|
58
|
+
sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let body: unknown;
|
|
62
|
+
try {
|
|
63
|
+
body = await parseJson(req);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err instanceof JsonBodyError) {
|
|
66
|
+
sendError(res, err.status, err.message);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
const parsed = parseCreateInput(body);
|
|
72
|
+
if (!parsed.ok) {
|
|
73
|
+
sendError(res, HTTP_STATUS.BAD_REQUEST, parsed.message);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const created = deps.service.create(parsed.value);
|
|
77
|
+
sendJson(res, HTTP_STATUS.CREATED, created);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async update(req: IncomingMessage, res: ServerResponse, params: Params): Promise<void> {
|
|
81
|
+
if (!deps.repositoryConfigured || !deps.service) {
|
|
82
|
+
sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let body: unknown;
|
|
86
|
+
try {
|
|
87
|
+
body = await parseJson(req);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err instanceof JsonBodyError) {
|
|
90
|
+
sendError(res, err.status, err.message);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
const parsed = parseUpdateInput(body);
|
|
96
|
+
if (!parsed.ok) {
|
|
97
|
+
sendError(res, HTTP_STATUS.BAD_REQUEST, parsed.message);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const updated = deps.service.update(params.id!, parsed.value);
|
|
101
|
+
if (!updated) {
|
|
102
|
+
sendError(res, HTTP_STATUS.NOT_FOUND, "Not found");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
sendJson(res, HTTP_STATUS.OK, updated);
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async remove(_req: IncomingMessage, res: ServerResponse, params: Params): Promise<void> {
|
|
109
|
+
if (!deps.repositoryConfigured || !deps.service) {
|
|
110
|
+
sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const ok = deps.service.remove(params.id!);
|
|
114
|
+
if (!ok) {
|
|
115
|
+
sendError(res, HTTP_STATUS.NOT_FOUND, "Not found");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
res.statusCode = HTTP_STATUS.NO_CONTENT;
|
|
119
|
+
res.end();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type <%= it.entityPascal %>Controller = ReturnType<typeof create<%= it.entityPascal %>Controller>;
|
|
125
|
+
<% } else { %>
|
|
126
|
+
export function create<%= it.entityPascal %>Controller(deps: { service: <%= it.entityPascal %>Service }) {
|
|
127
|
+
return {
|
|
128
|
+
async status(_req: IncomingMessage, res: ServerResponse, _params: Params): Promise<void> {
|
|
129
|
+
sendJson(res, HTTP_STATUS.OK, { resource: deps.service.status() });
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type <%= it.entityPascal %>Controller = ReturnType<typeof create<%= it.entityPascal %>Controller>;
|
|
135
|
+
<% } %>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { create<%= it.entityPascal %>Module } from "../src/<%= it.resourceKebab %>/<%= it.resourceKebab %>.module";
|
|
4
|
+
import { createFake<%= it.entityPascal %>Repository } from "./<%= it.resourceKebab %>.repository.fake";
|
|
5
|
+
|
|
6
|
+
describe("<%= it.resourceKebab %> module", () => {
|
|
7
|
+
it("lists a page when repository is wired", () => {
|
|
8
|
+
const repository = createFake<%= it.entityPascal %>Repository();
|
|
9
|
+
repository.create({} as never);
|
|
10
|
+
repository.create({} as never);
|
|
11
|
+
assert.equal(repository.findAll({ limit: 1, offset: 0 }).length, 1);
|
|
12
|
+
assert.equal(repository.findAll({ limit: 1, offset: 1 }).length, 1);
|
|
13
|
+
const mod = create<%= it.entityPascal %>Module({ repository });
|
|
14
|
+
assert.equal(mod.routes.length, 5);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<% if (it.crud) { %>
|
|
2
|
+
import type { <%= it.entityPascal %>Repository } from "./<%= it.resourceKebab %>.repository";
|
|
3
|
+
import { create<%= it.entityPascal %>Controller } from "./<%= it.resourceKebab %>.controller";
|
|
4
|
+
import { create<%= it.entityPascal %>Routes } from "./<%= it.resourceKebab %>.routes";
|
|
5
|
+
import { create<%= it.entityPascal %>Service } from "./<%= it.resourceKebab %>.service";
|
|
6
|
+
|
|
7
|
+
export function create<%= it.entityPascal %>Module(opts: { repository?: <%= it.entityPascal %>Repository }) {
|
|
8
|
+
const repositoryConfigured = opts.repository !== undefined;
|
|
9
|
+
const service = repositoryConfigured ? create<%= it.entityPascal %>Service(opts.repository!) : null;
|
|
10
|
+
const controller = create<%= it.entityPascal %>Controller({ service, repositoryConfigured });
|
|
11
|
+
const routes = create<%= it.entityPascal %>Routes(controller);
|
|
12
|
+
return { routes };
|
|
13
|
+
}
|
|
14
|
+
<% } else { %>
|
|
15
|
+
import { create<%= it.entityPascal %>Controller } from "./<%= it.resourceKebab %>.controller";
|
|
16
|
+
import { create<%= it.entityPascal %>Routes } from "./<%= it.resourceKebab %>.routes";
|
|
17
|
+
import { create<%= it.entityPascal %>Service } from "./<%= it.resourceKebab %>.service";
|
|
18
|
+
|
|
19
|
+
export function create<%= it.entityPascal %>Module() {
|
|
20
|
+
const service = create<%= it.entityPascal %>Service();
|
|
21
|
+
const controller = create<%= it.entityPascal %>Controller({ service });
|
|
22
|
+
const routes = create<%= it.entityPascal %>Routes(controller);
|
|
23
|
+
return { routes };
|
|
24
|
+
}
|
|
25
|
+
<% } %>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { <%= it.entityPascal %>, Create<%= it.entityPascal %>Input } from "../src/<%= it.resourceKebab %>/<%= it.resourceKebab %>.types";
|
|
2
|
+
import type { <%= it.entityPascal %>Repository } from "../src/<%= it.resourceKebab %>/<%= it.resourceKebab %>.repository";
|
|
3
|
+
import type { ListQuery } from "../src/app/list";
|
|
4
|
+
|
|
5
|
+
export function createFake<%= it.entityPascal %>Repository(): <%= it.entityPascal %>Repository {
|
|
6
|
+
const store = new Map<string, <%= it.entityPascal %>>();
|
|
7
|
+
let seq = 1;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
findAll(query: ListQuery) {
|
|
11
|
+
return [...store.values()].slice(query.offset, query.offset + query.limit);
|
|
12
|
+
},
|
|
13
|
+
findById(id: string) {
|
|
14
|
+
return store.get(id);
|
|
15
|
+
},
|
|
16
|
+
create(input: Create<%= it.entityPascal %>Input) {
|
|
17
|
+
const id = String(seq++);
|
|
18
|
+
const item = { id, ...input } as <%= it.entityPascal %>;
|
|
19
|
+
store.set(id, item);
|
|
20
|
+
return item;
|
|
21
|
+
},
|
|
22
|
+
update(id, input) {
|
|
23
|
+
const existing = store.get(id);
|
|
24
|
+
if (!existing) return undefined;
|
|
25
|
+
const next = { ...existing, ...input };
|
|
26
|
+
store.set(id, next);
|
|
27
|
+
return next;
|
|
28
|
+
},
|
|
29
|
+
delete(id) {
|
|
30
|
+
return store.delete(id);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { <%= it.entityPascal %> } from "./<%= it.resourceKebab %>.types";
|
|
2
|
+
<% if (it.crud) { %>
|
|
3
|
+
import type { ListQuery } from "../app/list";
|
|
4
|
+
<% } %>
|
|
5
|
+
|
|
6
|
+
export interface <%= it.entityPascal %>Repository {
|
|
7
|
+
<% if (it.crud) { %>
|
|
8
|
+
findAll(query: ListQuery): <%= it.entityPascal %>[];
|
|
9
|
+
findById(id: string): <%= it.entityPascal %> | undefined;
|
|
10
|
+
create(input: Omit<<%= it.entityPascal %>, "id">): <%= it.entityPascal %>;
|
|
11
|
+
update(id: string, input: Partial<Omit<<%= it.entityPascal %>, "id">>): <%= it.entityPascal %> | undefined;
|
|
12
|
+
delete(id: string): boolean;
|
|
13
|
+
<% } %>
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<% const basePath = it.routePrefix ? it.routePrefix + "/" + it.resourceKebab : "/" + it.resourceKebab; %>
|
|
2
|
+
import { HTTP_METHOD } from "../app/http";
|
|
3
|
+
import type { Route } from "../app/router";
|
|
4
|
+
import type { <%= it.entityPascal %>Controller } from "./<%= it.resourceKebab %>.controller";
|
|
5
|
+
|
|
6
|
+
export function create<%= it.entityPascal %>Routes(controller: <%= it.entityPascal %>Controller): Route[] {
|
|
7
|
+
return [
|
|
8
|
+
<% if (it.crud) { %>
|
|
9
|
+
{ method: HTTP_METHOD.GET, path: "<%= basePath %>", handler: controller.list },
|
|
10
|
+
{ method: HTTP_METHOD.GET, path: "<%= basePath %>/:id", handler: controller.getById },
|
|
11
|
+
{ method: HTTP_METHOD.POST, path: "<%= basePath %>", handler: controller.create },
|
|
12
|
+
{ method: HTTP_METHOD.PATCH, path: "<%= basePath %>/:id", handler: controller.update },
|
|
13
|
+
{ method: HTTP_METHOD.DELETE, path: "<%= basePath %>/:id", handler: controller.remove },
|
|
14
|
+
<% } else { %>
|
|
15
|
+
{ method: HTTP_METHOD.GET, path: "<%= basePath %>", handler: controller.status },
|
|
16
|
+
<% } %>
|
|
17
|
+
];
|
|
18
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<% if (it.crud) { %>
|
|
2
|
+
import type { <%= it.entityPascal %>Repository } from "./<%= it.resourceKebab %>.repository";
|
|
3
|
+
import type {
|
|
4
|
+
<%= it.entityPascal %>,
|
|
5
|
+
Create<%= it.entityPascal %>Input,
|
|
6
|
+
Update<%= it.entityPascal %>Input,
|
|
7
|
+
} from "./<%= it.resourceKebab %>.types";
|
|
8
|
+
import type { ListQuery } from "../app/list";
|
|
9
|
+
|
|
10
|
+
export function create<%= it.entityPascal %>Service(repository: <%= it.entityPascal %>Repository) {
|
|
11
|
+
return {
|
|
12
|
+
list(query: ListQuery): <%= it.entityPascal %>[] {
|
|
13
|
+
return repository.findAll(query);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
getById(id: string): <%= it.entityPascal %> | undefined {
|
|
17
|
+
return repository.findById(id);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(input: Create<%= it.entityPascal %>Input): <%= it.entityPascal %> {
|
|
21
|
+
return repository.create(input);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
update(id: string, input: Update<%= it.entityPascal %>Input): <%= it.entityPascal %> | undefined {
|
|
25
|
+
return repository.update(id, input);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
remove(id: string): boolean {
|
|
29
|
+
return repository.delete(id);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type <%= it.entityPascal %>Service = ReturnType<typeof create<%= it.entityPascal %>Service>;
|
|
35
|
+
<% } else { %>
|
|
36
|
+
export function create<%= it.entityPascal %>Service() {
|
|
37
|
+
return {
|
|
38
|
+
status(): string {
|
|
39
|
+
return "<%= it.resourceKebab %>";
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type <%= it.entityPascal %>Service = ReturnType<typeof create<%= it.entityPascal %>Service>;
|
|
45
|
+
<% } %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type <%= it.entityPascal %> = {
|
|
2
|
+
id: string;
|
|
3
|
+
<% if (it.crud) { it.fields.forEach(function(field) { %>
|
|
4
|
+
<%= field.name %>: <%= field.tsType %>;
|
|
5
|
+
<% }); } %>
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
<% if (it.crud) { %>
|
|
9
|
+
export type Create<%= it.entityPascal %>Input = Omit<<%= it.entityPascal %>, "id">;
|
|
10
|
+
|
|
11
|
+
export type Update<%= it.entityPascal %>Input = Partial<Omit<<%= it.entityPascal %>, "id">>;
|
|
12
|
+
<% } %>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Create<%= it.entityPascal %>Input, Update<%= it.entityPascal %>Input } from "./<%= it.resourceKebab %>.types";
|
|
2
|
+
|
|
3
|
+
/** Replace this module with Zod/Ajv/etc. when you outgrow hand-rolled checks. */
|
|
4
|
+
export type ValidateResult<T> = { ok: true; value: T } | { ok: false; message: string };
|
|
5
|
+
|
|
6
|
+
export function parseCreateInput(body: unknown): ValidateResult<Create<%= it.entityPascal %>Input> {
|
|
7
|
+
if (typeof body !== "object" || body === null) {
|
|
8
|
+
return { ok: false, message: "Invalid request body" };
|
|
9
|
+
}
|
|
10
|
+
const b = body as Record<string, unknown>;
|
|
11
|
+
<% if (it.fields.length === 0) { %>
|
|
12
|
+
for (const key of Object.keys(b)) {
|
|
13
|
+
return { ok: false, message: "Invalid request body" };
|
|
14
|
+
}
|
|
15
|
+
return { ok: true, value: {} as Create<%= it.entityPascal %>Input };
|
|
16
|
+
<% } else { %>
|
|
17
|
+
<% it.fields.filter(function(f) { return f.required; }).forEach(function(field) { %>
|
|
18
|
+
if (typeof b.<%= field.name %> !== "<%= field.tsType %>") {
|
|
19
|
+
return { ok: false, message: "Invalid request body" };
|
|
20
|
+
}
|
|
21
|
+
<% if (field.tsType === "string") { %>
|
|
22
|
+
if ((b.<%= field.name %> as string).length === 0) {
|
|
23
|
+
return { ok: false, message: "Invalid request body" };
|
|
24
|
+
}
|
|
25
|
+
<% } %>
|
|
26
|
+
<% }); %>
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
value: b as Create<%= it.entityPascal %>Input,
|
|
30
|
+
};
|
|
31
|
+
<% } %>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseUpdateInput(body: unknown): ValidateResult<Update<%= it.entityPascal %>Input> {
|
|
35
|
+
if (typeof body !== "object" || body === null) {
|
|
36
|
+
return { ok: false, message: "Invalid request body" };
|
|
37
|
+
}
|
|
38
|
+
<% if (it.fields.length === 0) { %>
|
|
39
|
+
return { ok: false, message: "No update fields configured" };
|
|
40
|
+
<% } else { %>
|
|
41
|
+
const b = body as Record<string, unknown>;
|
|
42
|
+
let hasField = false;
|
|
43
|
+
<% it.fields.forEach(function(field) { %>
|
|
44
|
+
if (b.<%= field.name %> !== undefined) {
|
|
45
|
+
hasField = true;
|
|
46
|
+
if (typeof b.<%= field.name %> !== "<%= field.tsType %>") {
|
|
47
|
+
return { ok: false, message: "Invalid request body" };
|
|
48
|
+
}
|
|
49
|
+
<% if (field.tsType === "string") { %>
|
|
50
|
+
if ((b.<%= field.name %> as string).length === 0) {
|
|
51
|
+
return { ok: false, message: "Invalid request body" };
|
|
52
|
+
}
|
|
53
|
+
<% } %>
|
|
54
|
+
}
|
|
55
|
+
<% }); %>
|
|
56
|
+
if (!hasField) {
|
|
57
|
+
return { ok: false, message: "Invalid request body" };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
value: b as Update<%= it.entityPascal %>Input,
|
|
62
|
+
};
|
|
63
|
+
<% } %>
|
|
64
|
+
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Aether CLI
|
|
2
|
+
|
|
3
|
+
> Source scaffolder for plain TypeScript HTTP backends on Node's built-in `http` module. Generated apps have zero runtime npm dependencies and do not require Aether at runtime.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Starting a new Node.js backend with TypeScript and `node:http`, not a runtime web framework
|
|
8
|
+
- You want generated source you own: router, middleware, composition root, CRUD layers
|
|
9
|
+
- Generating CRUD modules from an OpenAPI 3.0 specification
|
|
10
|
+
- Production deploy should be `tsc` + `node` with no application `dependencies`
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm i -g @malyuga/aether-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Node.js 20+.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
mkdir my-api && cd my-api
|
|
24
|
+
aether init
|
|
25
|
+
npm install
|
|
26
|
+
aether g resource users --crud
|
|
27
|
+
npm run dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
After `init`, `GET /health` works. CRUD routes are registered in `src/app/composition.ts`, but the generated repository is an interface only. Until you pass a real implementation to `createUserModule({ repository: … })`, CRUD returns HTTP 501 with `{ "error": "Repository not configured" }`.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
- `aether init` — scaffold project in the current directory (`-f`, `--dry-run`, `--diff`, `--json`)
|
|
35
|
+
- `aether g resource <name>` — resource module; `--crud` for full stack; `--tests` for fake repo + tests
|
|
36
|
+
- `aether g middleware <name>` — middleware stub; `--global` registers in `src/app.ts`
|
|
37
|
+
- `aether g openapi <spec>` — CRUD resources from OpenAPI 3.0 JSON/YAML
|
|
38
|
+
- `aether check` — verify composition and config layout
|
|
39
|
+
- `aether doctor` — Node version, project context, packaged templates
|
|
40
|
+
- `aether upgrade` — read-only drift report; does not rewrite code
|
|
41
|
+
- `aether help` — full terminal guide
|
|
42
|
+
|
|
43
|
+
Alias: `g` for `generate`. Global flags: `--json`, `--diff` (with `--dry-run`).
|
|
44
|
+
|
|
45
|
+
## Generated app notes
|
|
46
|
+
|
|
47
|
+
- CommonJS; local imports without file extensions
|
|
48
|
+
- Optional Bearer auth via `BEARER_TOKEN` in `.env` (wired by `init`; health stays public)
|
|
49
|
+
- OpenAPI re-run without `--force` updates `*.types.ts` and `*.validate.ts` only
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
- [README](https://github.com/MalyugaSensei/aether-cli/blob/main/README.md): user-facing overview and examples
|
|
54
|
+
- [npm package](https://www.npmjs.com/package/@malyuga/aether-cli): install and registry metadata
|
|
55
|
+
- [Repository](https://github.com/MalyugaSensei/aether-cli): source and issues
|
|
56
|
+
|
|
57
|
+
This file is for external coding agents. Repository `AGENTS.md` is for contributors developing the CLI itself, not for end-user scaffolding.
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@malyuga/aether-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Scaffold plain TypeScript HTTP backends on node:http with zero runtime dependencies; OpenAPI-to-CRUD and explicit composition wiring",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"aether",
|
|
7
|
+
"cli",
|
|
8
|
+
"scaffold",
|
|
9
|
+
"generator",
|
|
10
|
+
"backend-scaffold",
|
|
11
|
+
"typescript-backend",
|
|
12
|
+
"node-http",
|
|
13
|
+
"openapi",
|
|
14
|
+
"crud-generator",
|
|
15
|
+
"zero-runtime-dependencies",
|
|
16
|
+
"composition-root"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "MalyugaSensei",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/MalyugaSensei/aether-cli.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/MalyugaSensei/aether-cli/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/MalyugaSensei/aether-cli#readme",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"bin": {
|
|
30
|
+
"aether": "dist/cli.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"llms.txt"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc && node scripts/copy-templates.mjs",
|
|
41
|
+
"pretest": "npm run build",
|
|
42
|
+
"dev": "tsx src/cli.ts",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"test:e2e": "vitest run tests/integration/e2e-install.test.ts",
|
|
46
|
+
"prepublishOnly": "npm test",
|
|
47
|
+
"release:patch": "npm version patch",
|
|
48
|
+
"release:minor": "npm version minor",
|
|
49
|
+
"release:major": "npm version major"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"commander": "^15.0.0",
|
|
56
|
+
"eta": "^4.6.0",
|
|
57
|
+
"prettier": "^3.9.7",
|
|
58
|
+
"ts-morph": "^28.0.0",
|
|
59
|
+
"yaml": "^2.9.1"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^22.20.3",
|
|
63
|
+
"tsx": "^4.23.13",
|
|
64
|
+
"typescript": "^7.0.2",
|
|
65
|
+
"vitest": "^5.0.1"
|
|
66
|
+
}
|
|
67
|
+
}
|