@apso/cli 0.16.1 → 0.18.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.
@@ -13,7 +13,7 @@ class Generate extends base_command_1.default {
13
13
  const { flags } = await this.parse(Generate);
14
14
  const skipFormat = flags["skip-format"];
15
15
  const totalBuildStart = perf_hooks_1.performance.now();
16
- const { rootFolder, entities, relationshipMap, apiType, auth, language: configLanguage } = (0, lib_1.parseApsorc)();
16
+ const { rootFolder, entities, relationshipMap, apiType, auth, emitEvents, language: configLanguage } = (0, lib_1.parseApsorc)();
17
17
  // Resolve language: flag > .apsorc > prompt
18
18
  let language;
19
19
  if (flags.language) {
@@ -63,6 +63,7 @@ class Generate extends base_command_1.default {
63
63
  entities,
64
64
  relationshipMap,
65
65
  auth,
66
+ emitEvents,
66
67
  };
67
68
  const generator = (0, lib_1.createGenerator)(generatorConfig);
68
69
  const validationResult = generator.validateConfig(generatorConfig);
@@ -148,8 +149,15 @@ class Generate extends base_command_1.default {
148
149
  await (0, file_system_1.createFile)(fullPath, file.content);
149
150
  }
150
151
  generatedFileCount += guardFiles.length;
152
+ // Generate domain-event spine (only when at least one entity opts in)
153
+ const domainEventFiles = await generator.generateDomainEvents(entities, lowerCaseApiType, { emitEvents });
154
+ for (const file of domainEventFiles) {
155
+ const fullPath = path.join(autogenPath, file.path);
156
+ // eslint-disable-next-line no-await-in-loop
157
+ await (0, file_system_1.createFile)(fullPath, file.content);
158
+ }
151
159
  // Generate index module
152
- const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType);
160
+ const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { emitEvents });
153
161
  for (const file of indexFiles) {
154
162
  const fullPath = path.join(autogenPath, file.path);
155
163
  // eslint-disable-next-line no-await-in-loop
@@ -923,6 +923,7 @@ const INLINE_SCHEMA_REFERENCE = `# Schema Quick Reference
923
923
  "updated_at": true,
924
924
  "primaryKeyType": "serial",
925
925
  "scopeBy": "organizationId",
926
+ "emitEvents": true,
926
927
  "fields": [
927
928
  { "name": "fieldName", "type": "text", "length": 100 }
928
929
  ],
@@ -932,6 +933,19 @@ const INLINE_SCHEMA_REFERENCE = `# Schema Quick Reference
932
933
  }
933
934
  \`\`\`
934
935
 
936
+ ## Domain Events (emitEvents)
937
+ Set \`emitEvents: true\` (per-entity or as a top-level default) to durably log
938
+ state changes. A top-level \`emitEvents: true\` enables it for every entity; an
939
+ individual entity can opt out with \`emitEvents: false\` (effective value is
940
+ \`entity.emitEvents ?? <top-level emitEvents> ?? false\`).
941
+
942
+ When at least one entity opts in, the generator emits a single schema-derived
943
+ manifest at \`autogen/events/event-emitting.entities.ts\` exporting
944
+ \`EVENT_EMITTING_ENTITIES\` (the opted-in entity classes) and
945
+ \`EVENT_EMITTING_ENTITY_NAMES\`. The domain-event engine itself ships in the
946
+ \`@apso/domain-events\` library and is wired by the \`domain-events\` skill; the
947
+ CLI no longer generates the engine code.
948
+
935
949
  ## Relationship Types
936
950
  - OneToMany: parent has many children
937
951
  - ManyToOne: child belongs to parent
@@ -14,6 +14,12 @@ export type ApsorcType = {
14
14
  relationships: ApsorcRelationship[];
15
15
  auth?: AuthConfig;
16
16
  language?: TargetLanguage;
17
+ /**
18
+ * Top-level default for the opt-in DomainEvent ("emitEvents") feature.
19
+ * When true, every entity emits domain events unless it opts out with
20
+ * its own `emitEvents: false`.
21
+ */
22
+ emitEvents?: boolean;
17
23
  };
18
24
  type ParsedApsorcData = {
19
25
  entities: Entity[];
@@ -26,6 +32,8 @@ type ParsedApsorc = {
26
32
  relationshipMap: RelationshipMap;
27
33
  auth?: AuthConfig;
28
34
  language?: TargetLanguage;
35
+ /** Top-level default for the DomainEvent ("emitEvents") feature. */
36
+ emitEvents?: boolean;
29
37
  };
30
38
  export declare const parseApsorcV1: (apsorc: ApsorcType) => ParsedApsorcData;
31
39
  export declare const parseApsorcV2: (apsorc: ApsorcType) => ParsedApsorcData;
@@ -36,6 +36,7 @@ const parseRc = () => {
36
36
  const relationships = apsoConfig.relationships || [];
37
37
  const auth = apsoConfig.auth;
38
38
  const language = apsoConfig.language;
39
+ const emitEvents = apsoConfig.emitEvents;
39
40
  return {
40
41
  rootFolder,
41
42
  apiType,
@@ -44,6 +45,7 @@ const parseRc = () => {
44
45
  relationships,
45
46
  auth,
46
47
  language,
48
+ emitEvents,
47
49
  };
48
50
  };
49
51
  const parseApsorc = () => {
@@ -63,6 +65,7 @@ const parseApsorc = () => {
63
65
  apiType: apsoConfig.apiType,
64
66
  auth: apsoConfig.auth,
65
67
  language: apsoConfig.language,
68
+ emitEvents: apsoConfig.emitEvents,
66
69
  ...(0, exports.parseApsorcV1)(apsoConfig),
67
70
  };
68
71
  if (debug) {
@@ -79,6 +82,7 @@ const parseApsorc = () => {
79
82
  apiType: apsoConfig.apiType,
80
83
  auth: apsoConfig.auth,
81
84
  language: apsoConfig.language,
85
+ emitEvents: apsoConfig.emitEvents,
82
86
  ...(() => {
83
87
  const relStart = perf_hooks_1.performance.now();
84
88
  const parsed = (0, exports.parseApsorcV2)(apsoConfig);
@@ -0,0 +1,32 @@
1
+ import { Entity } from "./types";
2
+ /**
3
+ * Helpers for the opt-in DomainEvent ("emitEvents") feature.
4
+ *
5
+ * Under the Apso Distribution Model the domain-event engine ships in the
6
+ * `@apso/domain-events` library (wired by the `domain-events` skill). The CLI
7
+ * only uses these helpers to compute which entities opted in, and emits a
8
+ * schema-derived manifest (`events/event-emitting.entities.ts`).
9
+ *
10
+ * Flag resolution: the effective per-entity value is
11
+ * `entity.emitEvents ?? globalEmitEvents ?? false`
12
+ * so a top-level `emitEvents: true` enables it for every entity, while an
13
+ * individual entity can opt out with `emitEvents: false` (global default WITH
14
+ * per-entity opt-out).
15
+ */
16
+ /**
17
+ * Computes whether a single entity has domain events enabled, given the
18
+ * top-level (global) default.
19
+ */
20
+ export declare function isEmitEventsEnabled(entity: Entity, globalEmitEvents?: boolean): boolean;
21
+ /**
22
+ * Computes the set of entities that have opted in to domain-event emission.
23
+ * This is the single source of truth used everywhere (generator, wiring,
24
+ * subscriber scoping).
25
+ */
26
+ export declare function getEventEmittingEntities(entities: Entity[], globalEmitEvents?: boolean): Entity[];
27
+ /**
28
+ * Returns true when at least one entity has domain events enabled, meaning the
29
+ * event-emitting manifest (`events/event-emitting.entities.ts`) should be
30
+ * generated.
31
+ */
32
+ export declare function hasEventEmittingEntities(entities: Entity[], globalEmitEvents?: boolean): boolean;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasEventEmittingEntities = exports.getEventEmittingEntities = exports.isEmitEventsEnabled = void 0;
4
+ /**
5
+ * Helpers for the opt-in DomainEvent ("emitEvents") feature.
6
+ *
7
+ * Under the Apso Distribution Model the domain-event engine ships in the
8
+ * `@apso/domain-events` library (wired by the `domain-events` skill). The CLI
9
+ * only uses these helpers to compute which entities opted in, and emits a
10
+ * schema-derived manifest (`events/event-emitting.entities.ts`).
11
+ *
12
+ * Flag resolution: the effective per-entity value is
13
+ * `entity.emitEvents ?? globalEmitEvents ?? false`
14
+ * so a top-level `emitEvents: true` enables it for every entity, while an
15
+ * individual entity can opt out with `emitEvents: false` (global default WITH
16
+ * per-entity opt-out).
17
+ */
18
+ /**
19
+ * Computes whether a single entity has domain events enabled, given the
20
+ * top-level (global) default.
21
+ */
22
+ function isEmitEventsEnabled(entity, globalEmitEvents) {
23
+ var _a, _b;
24
+ return (_b = (_a = entity.emitEvents) !== null && _a !== void 0 ? _a : globalEmitEvents) !== null && _b !== void 0 ? _b : false;
25
+ }
26
+ exports.isEmitEventsEnabled = isEmitEventsEnabled;
27
+ /**
28
+ * Computes the set of entities that have opted in to domain-event emission.
29
+ * This is the single source of truth used everywhere (generator, wiring,
30
+ * subscriber scoping).
31
+ */
32
+ function getEventEmittingEntities(entities, globalEmitEvents) {
33
+ return entities.filter((entity) => isEmitEventsEnabled(entity, globalEmitEvents));
34
+ }
35
+ exports.getEventEmittingEntities = getEventEmittingEntities;
36
+ /**
37
+ * Returns true when at least one entity has domain events enabled, meaning the
38
+ * event-emitting manifest (`events/event-emitting.entities.ts`) should be
39
+ * generated.
40
+ */
41
+ function hasEventEmittingEntities(entities, globalEmitEvents) {
42
+ return entities.some((entity) => isEmitEventsEnabled(entity, globalEmitEvents));
43
+ }
44
+ exports.hasEventEmittingEntities = hasEventEmittingEntities;
@@ -52,7 +52,9 @@ export declare abstract class BaseGenerator implements LanguageGenerator {
52
52
  /**
53
53
  * Generate the index/main module file
54
54
  */
55
- abstract generateIndexModule(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
55
+ abstract generateIndexModule(entities: Entity[], apiType: string, opts?: {
56
+ emitEvents?: boolean;
57
+ }): Promise<GeneratedFile[]>;
56
58
  /**
57
59
  * Generate guard files (auth, scope, etc.)
58
60
  */
@@ -62,6 +64,15 @@ export declare abstract class BaseGenerator implements LanguageGenerator {
62
64
  * Default no-op. Override in language generators that need query utils.
63
65
  */
64
66
  generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
67
+ /**
68
+ * Emit a schema-derived manifest of entities that opt in to `emitEvents`.
69
+ * The domain-event engine ships in the `@apso/domain-events` library (wired
70
+ * by the `domain-events` skill); the generator only declares participants.
71
+ * Default no-op. Override in language generators that support it.
72
+ */
73
+ generateDomainEvents(_entities: Entity[], _apiType?: string, _opts?: {
74
+ emitEvents?: boolean;
75
+ }): Promise<GeneratedFile[]>;
65
76
  /**
66
77
  * Render a template file with the given data
67
78
  */
@@ -27,6 +27,15 @@ class BaseGenerator {
27
27
  async generateQueryUtils(_entities, _apiType) {
28
28
  return [];
29
29
  }
30
+ /**
31
+ * Emit a schema-derived manifest of entities that opt in to `emitEvents`.
32
+ * The domain-event engine ships in the `@apso/domain-events` library (wired
33
+ * by the `domain-events` skill); the generator only declares participants.
34
+ * Default no-op. Override in language generators that support it.
35
+ */
36
+ async generateDomainEvents(_entities, _apiType, _opts) {
37
+ return [];
38
+ }
30
39
  /**
31
40
  * Render a template file with the given data
32
41
  */
@@ -16,8 +16,30 @@ export declare class TypeScriptGenerator extends BaseGenerator {
16
16
  generateModule(options: EntityGenerationOptions): Promise<GeneratedFile[]>;
17
17
  generateMigration(_options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
18
18
  generateEnums(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
19
- generateIndexModule(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
19
+ generateIndexModule(entities: Entity[], apiType: string, _opts?: {
20
+ emitEvents?: boolean;
21
+ }): Promise<GeneratedFile[]>;
20
22
  generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
23
+ /**
24
+ * Emits a schema-derived manifest of the entities that have opted in to
25
+ * domain-event emission (`.apsorc` `emitEvents`).
26
+ *
27
+ * Under the Apso Distribution Model the domain-event engine no longer ships
28
+ * as generated code; it lives in the `@apso/domain-events` library and is
29
+ * wired by the `domain-events` skill. The CLI's only responsibility is to
30
+ * declare WHICH entities participate, via a single generated file:
31
+ * `events/event-emitting.entities.ts`.
32
+ *
33
+ * Returns an empty array when no entity is opted in, so callers can skip
34
+ * wiring entirely.
35
+ *
36
+ * @param entities All entities from the configuration
37
+ * @param _apiType API type (unused; events are REST/Graphql agnostic)
38
+ * @param opts.emitEvents Top-level (global) default for emitEvents
39
+ */
40
+ generateDomainEvents(entities: Entity[], _apiType?: string, opts?: {
41
+ emitEvents?: boolean;
42
+ }): Promise<GeneratedFile[]>;
21
43
  /**
22
44
  * Normalizes auth config by applying defaults for DB session providers
23
45
  */
@@ -15,6 +15,7 @@ const fs = tslib_1.__importStar(require("fs"));
15
15
  const os = tslib_1.__importStar(require("os"));
16
16
  const util_1 = require("util");
17
17
  const guards_1 = require("../guards");
18
+ const events_1 = require("../events");
18
19
  const auth_1 = require("../types/auth");
19
20
  const unlinkAsync = (0, util_1.promisify)(fs.unlink);
20
21
  /**
@@ -302,7 +303,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
302
303
  },
303
304
  ];
304
305
  }
305
- async generateIndexModule(entities, apiType) {
306
+ async generateIndexModule(entities, apiType, _opts) {
306
307
  const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, { entities });
307
308
  return [
308
309
  {
@@ -355,6 +356,40 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
355
356
  });
356
357
  return files;
357
358
  }
359
+ /**
360
+ * Emits a schema-derived manifest of the entities that have opted in to
361
+ * domain-event emission (`.apsorc` `emitEvents`).
362
+ *
363
+ * Under the Apso Distribution Model the domain-event engine no longer ships
364
+ * as generated code; it lives in the `@apso/domain-events` library and is
365
+ * wired by the `domain-events` skill. The CLI's only responsibility is to
366
+ * declare WHICH entities participate, via a single generated file:
367
+ * `events/event-emitting.entities.ts`.
368
+ *
369
+ * Returns an empty array when no entity is opted in, so callers can skip
370
+ * wiring entirely.
371
+ *
372
+ * @param entities All entities from the configuration
373
+ * @param _apiType API type (unused; events are REST/Graphql agnostic)
374
+ * @param opts.emitEvents Top-level (global) default for emitEvents
375
+ */
376
+ async generateDomainEvents(entities, _apiType, opts) {
377
+ const emittingEntities = (0, events_1.getEventEmittingEntities)(entities, opts === null || opts === void 0 ? void 0 : opts.emitEvents);
378
+ if (emittingEntities.length === 0) {
379
+ return [];
380
+ }
381
+ const content = await this.renderTemplate("./events/event-emitting.entities.eta", {
382
+ emittingEntities: emittingEntities.map((entity) => ({
383
+ name: entity.name,
384
+ })),
385
+ });
386
+ return [
387
+ {
388
+ path: "events/event-emitting.entities.ts",
389
+ content,
390
+ },
391
+ ];
392
+ }
358
393
  /**
359
394
  * Normalizes auth config by applying defaults for DB session providers
360
395
  */
@@ -1,5 +1,6 @@
1
1
  export { Entity, RelationshipMap } from "./types";
2
2
  export { parseApsorc } from "./apsorc-parser";
3
3
  export { hasScopedEntities, getScopedEntities } from "./guards";
4
+ export { isEmitEventsEnabled, getEventEmittingEntities, hasEventEmittingEntities, } from "./events";
4
5
  export { BaseGenerator, TypeScriptGenerator, createGenerator, isLanguageSupported, getSupportedLanguages, getImplementedLanguages, } from "./generators";
5
6
  export type { TargetLanguage, GeneratedFile, ValidationResult, GeneratorConfig, LanguageGenerator, } from "./types";
package/dist/lib/index.js CHANGED
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getImplementedLanguages = exports.getSupportedLanguages = exports.isLanguageSupported = exports.createGenerator = exports.TypeScriptGenerator = exports.BaseGenerator = exports.getScopedEntities = exports.hasScopedEntities = exports.parseApsorc = void 0;
3
+ exports.getImplementedLanguages = exports.getSupportedLanguages = exports.isLanguageSupported = exports.createGenerator = exports.TypeScriptGenerator = exports.BaseGenerator = exports.hasEventEmittingEntities = exports.getEventEmittingEntities = exports.isEmitEventsEnabled = exports.getScopedEntities = exports.hasScopedEntities = exports.parseApsorc = void 0;
4
4
  var apsorc_parser_1 = require("./apsorc-parser");
5
5
  Object.defineProperty(exports, "parseApsorc", { enumerable: true, get: function () { return apsorc_parser_1.parseApsorc; } });
6
6
  var guards_1 = require("./guards");
7
7
  Object.defineProperty(exports, "hasScopedEntities", { enumerable: true, get: function () { return guards_1.hasScopedEntities; } });
8
8
  Object.defineProperty(exports, "getScopedEntities", { enumerable: true, get: function () { return guards_1.getScopedEntities; } });
9
+ var events_1 = require("./events");
10
+ Object.defineProperty(exports, "isEmitEventsEnabled", { enumerable: true, get: function () { return events_1.isEmitEventsEnabled; } });
11
+ Object.defineProperty(exports, "getEventEmittingEntities", { enumerable: true, get: function () { return events_1.getEventEmittingEntities; } });
12
+ Object.defineProperty(exports, "hasEventEmittingEntities", { enumerable: true, get: function () { return events_1.hasEventEmittingEntities; } });
9
13
  // Generator exports
10
14
  var generators_1 = require("./generators");
11
15
  Object.defineProperty(exports, "BaseGenerator", { enumerable: true, get: function () { return generators_1.BaseGenerator; } });
@@ -0,0 +1,23 @@
1
+ <%~ includeFile('../header.eta') %>
2
+
3
+ <% it.emittingEntities.forEach((entity) => { %>
4
+ import { <%= entity.name %> } from '../<%= entity.name %>/<%= entity.name %>.entity';
5
+ <%}) %>
6
+
7
+ /**
8
+ * Schema-derived manifest of entities that opted in to domain-event emission
9
+ * (`.apsorc` `emitEvents`). The domain-event engine itself lives in the
10
+ * `@apso/domain-events` library and is wired by the `domain-events` skill;
11
+ * this file only declares WHICH entities participate.
12
+ */
13
+ export const EVENT_EMITTING_ENTITIES = [
14
+ <% it.emittingEntities.forEach((entity) => { %>
15
+ <%= entity.name %>,
16
+ <%}) %>
17
+ ];
18
+
19
+ export const EVENT_EMITTING_ENTITY_NAMES = [
20
+ <% it.emittingEntities.forEach((entity) => { %>
21
+ '<%= entity.name %>',
22
+ <%}) %>
23
+ ];
@@ -45,5 +45,15 @@ export interface Entity {
45
45
  * Optional configuration for how scope enforcement should behave.
46
46
  */
47
47
  scopeOptions?: ScopeOptions;
48
+ /**
49
+ * When true, state changes to this entity (insert/update/remove) emit a
50
+ * durable DomainEvent row written in the SAME database transaction as the
51
+ * change (transactional-outbox pattern, surfaced as generic "domain events").
52
+ *
53
+ * Resolution: the effective value is `entity.emitEvents ?? <global emitEvents> ?? false`.
54
+ * This means a top-level `emitEvents: true` enables it for every entity, while
55
+ * an individual entity can opt out with `emitEvents: false`.
56
+ */
57
+ emitEvents?: boolean;
48
58
  associations?: Association[];
49
59
  }
@@ -104,6 +104,11 @@ export interface GeneratorConfig {
104
104
  * Authentication configuration (optional)
105
105
  */
106
106
  auth?: AuthConfig;
107
+ /**
108
+ * Top-level default for the opt-in DomainEvent ("emitEvents") feature.
109
+ * Effective per-entity value is `entity.emitEvents ?? emitEvents ?? false`.
110
+ */
111
+ emitEvents?: boolean;
107
112
  /**
108
113
  * Language-specific configuration options
109
114
  */
@@ -229,7 +234,9 @@ export interface LanguageGenerator {
229
234
  /**
230
235
  * Generate the index/main module file that ties everything together
231
236
  */
232
- generateIndexModule(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
237
+ generateIndexModule(entities: Entity[], apiType: string, opts?: {
238
+ emitEvents?: boolean;
239
+ }): Promise<GeneratedFile[]>;
233
240
  /**
234
241
  * Generate guard files (auth, scope, etc.)
235
242
  */
@@ -238,6 +245,13 @@ export interface LanguageGenerator {
238
245
  * Generate shared query utility files (filter, sort, pagination)
239
246
  */
240
247
  generateQueryUtils(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
248
+ /**
249
+ * Emit a schema-derived manifest of entities that opt in to `emitEvents`
250
+ * (engine lives in `@apso/domain-events`). Returns [] when none are opted in.
251
+ */
252
+ generateDomainEvents(entities: Entity[], apiType?: string, opts?: {
253
+ emitEvents?: boolean;
254
+ }): Promise<GeneratedFile[]>;
241
255
  }
242
256
  /**
243
257
  * Factory function type for creating language generators
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.16.1",
9
+ "version": "0.18.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.16.1",
2
+ "version": "0.18.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.16.1",
3
+ "version": "0.18.0",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",