@apso/cli 0.18.0 → 0.19.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.
@@ -10,10 +10,11 @@ const perf_hooks_1 = require("perf_hooks");
10
10
  const file_system_1 = require("../lib/utils/file-system");
11
11
  class Generate extends base_command_1.default {
12
12
  async run() {
13
+ var _a, _b;
13
14
  const { flags } = await this.parse(Generate);
14
15
  const skipFormat = flags["skip-format"];
15
16
  const totalBuildStart = perf_hooks_1.performance.now();
16
- const { rootFolder, entities, relationshipMap, apiType, auth, emitEvents, language: configLanguage } = (0, lib_1.parseApsorc)();
17
+ const { rootFolder, entities, relationshipMap, apiType, auth, emitEvents, http, language: configLanguage } = (0, lib_1.parseApsorc)();
17
18
  // Resolve language: flag > .apsorc > prompt
18
19
  let language;
19
20
  if (flags.language) {
@@ -64,6 +65,7 @@ class Generate extends base_command_1.default {
64
65
  relationshipMap,
65
66
  auth,
66
67
  emitEvents,
68
+ http,
67
69
  };
68
70
  const generator = (0, lib_1.createGenerator)(generatorConfig);
69
71
  const validationResult = generator.validateConfig(generatorConfig);
@@ -94,8 +96,21 @@ class Generate extends base_command_1.default {
94
96
  console.log(`[apso] Building... ${entity.name}`);
95
97
  const entityBuildStart = perf_hooks_1.performance.now();
96
98
  const entityRelationships = relationshipMap[entity.name] || [];
97
- // eslint-disable-next-line no-await-in-loop
98
- const allFiles = await Promise.all([
99
+ // Effective HTTP-controller flag (TypeScript REST only for now): entity
100
+ // overrides the top-level default, which defaults to true. When false, the
101
+ // controller is neither generated nor wired into the module (a hand-written
102
+ // controller owns the route). GraphQL uses resolvers, so the flag doesn't
103
+ // apply there. Python (FastAPI) and Go (Gin) wire routers differently and
104
+ // don't yet honor this flag — keep it a no-op for them (controllers always
105
+ // generated) so we never emit broken wiring (a skipped router still
106
+ // referenced by the index module). Tracked separately for Python/Go.
107
+ const includeController = language === "typescript" && lowerCaseApiType === "rest"
108
+ ? ((_b = (_a = entity.http) !== null && _a !== void 0 ? _a : http) !== null && _b !== void 0 ? _b : true)
109
+ : true;
110
+ if (!includeController) {
111
+ console.log(`[apso] ${entity.name}: http=false — skipping generated controller`);
112
+ }
113
+ const generationTasks = [
99
114
  generator.generateEntity({
100
115
  entity,
101
116
  relationships: entityRelationships,
@@ -115,20 +130,25 @@ class Generate extends base_command_1.default {
115
130
  apiType: lowerCaseApiType,
116
131
  relationshipMap,
117
132
  }),
118
- generator.generateController({
133
+ generator.generateModule({
119
134
  entity,
120
135
  relationships: entityRelationships,
121
136
  allEntities: entities,
122
137
  apiType: lowerCaseApiType,
123
- relationshipMap,
138
+ includeController,
124
139
  }),
125
- generator.generateModule({
140
+ ];
141
+ if (includeController) {
142
+ generationTasks.push(generator.generateController({
126
143
  entity,
127
144
  relationships: entityRelationships,
128
145
  allEntities: entities,
129
146
  apiType: lowerCaseApiType,
130
- }),
131
- ]);
147
+ relationshipMap,
148
+ }));
149
+ }
150
+ // eslint-disable-next-line no-await-in-loop
151
+ const allFiles = await Promise.all(generationTasks);
132
152
  // eslint-disable-next-line no-await-in-loop
133
153
  for (const files of allFiles) {
134
154
  for (const file of files) {
@@ -924,6 +924,7 @@ const INLINE_SCHEMA_REFERENCE = `# Schema Quick Reference
924
924
  "primaryKeyType": "serial",
925
925
  "scopeBy": "organizationId",
926
926
  "emitEvents": true,
927
+ "http": false,
927
928
  "fields": [
928
929
  { "name": "fieldName", "type": "text", "length": 100 }
929
930
  ],
@@ -946,6 +947,20 @@ manifest at \`autogen/events/event-emitting.entities.ts\` exporting
946
947
  \`@apso/domain-events\` library and is wired by the \`domain-events\` skill; the
947
948
  CLI no longer generates the engine code.
948
949
 
950
+ ## HTTP controllers (http)
951
+ Controllers are generated by default. Set \`http: false\` on an entity (or a
952
+ top-level \`http: false\` default) to NOT generate/mount its HTTP controller while
953
+ still generating the entity, service, DTOs, and module (\`TypeOrmModule.forFeature\`
954
+ + the service). Use this when a hand-written controller owns the route for that
955
+ entity (e.g. a custom/Stripe-shaped surface). Effective value is
956
+ \`entity.http ?? <top-level http> ?? true\`.
957
+
958
+ Per framework: NestJS and Gin (Go) actually suppress the controller — those
959
+ frameworks can't cleanly override a generated route. For FastAPI it's a no-op:
960
+ override by registering your router ahead of the generated \`autogen_router\`
961
+ (FastAPI matches in registration order). Currently implemented for NestJS; Go
962
+ is tracked separately.
963
+
949
964
  ## Relationship Types
950
965
  - OneToMany: parent has many children
951
966
  - ManyToOne: child belongs to parent
@@ -20,6 +20,11 @@ export type ApsorcType = {
20
20
  * its own `emitEvents: false`.
21
21
  */
22
22
  emitEvents?: boolean;
23
+ /**
24
+ * Top-level default for HTTP controller generation. When false, no entity
25
+ * gets a generated controller unless it opts back in with `http: true`.
26
+ */
27
+ http?: boolean;
23
28
  };
24
29
  type ParsedApsorcData = {
25
30
  entities: Entity[];
@@ -34,6 +39,8 @@ type ParsedApsorc = {
34
39
  language?: TargetLanguage;
35
40
  /** Top-level default for the DomainEvent ("emitEvents") feature. */
36
41
  emitEvents?: boolean;
42
+ /** Top-level default for HTTP controller generation. */
43
+ http?: boolean;
37
44
  };
38
45
  export declare const parseApsorcV1: (apsorc: ApsorcType) => ParsedApsorcData;
39
46
  export declare const parseApsorcV2: (apsorc: ApsorcType) => ParsedApsorcData;
@@ -37,6 +37,7 @@ const parseRc = () => {
37
37
  const auth = apsoConfig.auth;
38
38
  const language = apsoConfig.language;
39
39
  const emitEvents = apsoConfig.emitEvents;
40
+ const http = apsoConfig.http;
40
41
  return {
41
42
  rootFolder,
42
43
  apiType,
@@ -46,6 +47,7 @@ const parseRc = () => {
46
47
  auth,
47
48
  language,
48
49
  emitEvents,
50
+ http,
49
51
  };
50
52
  };
51
53
  const parseApsorc = () => {
@@ -66,6 +68,7 @@ const parseApsorc = () => {
66
68
  auth: apsoConfig.auth,
67
69
  language: apsoConfig.language,
68
70
  emitEvents: apsoConfig.emitEvents,
71
+ http: apsoConfig.http,
69
72
  ...(0, exports.parseApsorcV1)(apsoConfig),
70
73
  };
71
74
  if (debug) {
@@ -83,6 +86,7 @@ const parseApsorc = () => {
83
86
  auth: apsoConfig.auth,
84
87
  language: apsoConfig.language,
85
88
  emitEvents: apsoConfig.emitEvents,
89
+ http: apsoConfig.http,
86
90
  ...(() => {
87
91
  const relStart = perf_hooks_1.performance.now();
88
92
  const parsed = (0, exports.parseApsorcV2)(apsoConfig);
@@ -249,7 +249,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
249
249
  ];
250
250
  }
251
251
  async generateModule(options) {
252
- const { entity, apiType } = options;
252
+ const { entity, apiType, includeController = true } = options;
253
253
  const { name: entityName } = entity;
254
254
  const moduleName = `${entityName}Module`;
255
255
  const svcName = `${entityName}Service`;
@@ -261,6 +261,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
261
261
  ctrlName,
262
262
  resolverName,
263
263
  entityName,
264
+ includeController,
264
265
  };
265
266
  const templatePath = apiType === "graphql"
266
267
  ? "./graphql/gql-module-graphql"
@@ -4,13 +4,12 @@ import { Module } from "@nestjs/common";
4
4
  import {<%= it.entityName %>} from './<%= it.entityName%>.entity'
5
5
  import { TypeOrmModule } from "@nestjs/typeorm";
6
6
  import {<%= it.svcName %>} from "./<%= it.entityName %>.service";
7
- import {<%= it.ctrlName %>} from "./<%= it.entityName %>.controller";
8
-
7
+ <% if (it.includeController) { %>import {<%= it.ctrlName %>} from "./<%= it.entityName %>.controller";
8
+ <% } %>
9
9
  @Module({
10
10
  imports: [TypeOrmModule.forFeature([<%= it.entityName %>])],
11
11
  providers: [<%= it.svcName %>],
12
12
  exports: [<%= it.svcName %>],
13
- controllers: [<%= it.ctrlName %>],
14
- })
13
+ <% if (it.includeController) { %> controllers: [<%= it.ctrlName %>],
14
+ <% } %>})
15
15
  export class <%= it.moduleName %> {}
16
-
@@ -55,5 +55,26 @@ export interface Entity {
55
55
  * an individual entity can opt out with `emitEvents: false`.
56
56
  */
57
57
  emitEvents?: boolean;
58
+ /**
59
+ * Whether to generate the HTTP controller for this entity. Defaults to true.
60
+ * When false, the generator still emits the entity, service, DTOs, and a module
61
+ * (with `TypeOrmModule.forFeature` + the service), but omits the controller —
62
+ * leaving the HTTP surface to a hand-written controller without a route
63
+ * collision with the generated CRUD.
64
+ *
65
+ * Per-framework behavior (the flag is generic, but only matters where a
66
+ * generated route can't be cleanly overridden):
67
+ * - **NestJS** and **Gin (Go)**: actually suppresses the controller/route —
68
+ * these frameworks can't override a generated route (Nest controllers aren't
69
+ * DI-swappable; Gin panics on duplicate paths). Implemented for NestJS;
70
+ * Go is tracked separately.
71
+ * - **FastAPI**: effectively a **no-op** — FastAPI matches routes in
72
+ * registration order, so an app overrides by registering its router ahead of
73
+ * the generated `autogen_router` (no suppression needed).
74
+ *
75
+ * Resolution: the effective value is `entity.http ?? <global http> ?? true`, so
76
+ * controllers are on by default and opted out per entity (or globally).
77
+ */
78
+ http?: boolean;
58
79
  associations?: Association[];
59
80
  }
@@ -109,6 +109,11 @@ export interface GeneratorConfig {
109
109
  * Effective per-entity value is `entity.emitEvents ?? emitEvents ?? false`.
110
110
  */
111
111
  emitEvents?: boolean;
112
+ /**
113
+ * Top-level default for HTTP controller generation.
114
+ * Effective per-entity value is `entity.http ?? http ?? true`.
115
+ */
116
+ http?: boolean;
112
117
  /**
113
118
  * Language-specific configuration options
114
119
  */
@@ -134,6 +139,11 @@ export interface EntityGenerationOptions {
134
139
  * API type being generated
135
140
  */
136
141
  apiType: string;
142
+ /**
143
+ * Whether the entity's HTTP controller is generated/wired. Defaults to true.
144
+ * When false, the module omits the controller (see Entity.http).
145
+ */
146
+ includeController?: boolean;
137
147
  }
138
148
  /**
139
149
  * Options for generating controllers/routers
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.18.0",
9
+ "version": "0.19.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@electric-sql/pglite": "^0.2.17",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.18.0",
2
+ "version": "0.19.0",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",