@magek/core 0.0.7 → 0.0.9

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildClassMetadataFromFields = buildClassMetadataFromFields;
4
4
  const decorator_types_1 = require("./decorator-types");
5
5
  const read_model_1 = require("./read-model");
6
+ const returns_1 = require("./returns");
6
7
  /**
7
8
  * Extract TypeMetadata from a @field() decorator's metadata
8
9
  */
@@ -200,6 +201,16 @@ function buildClassMetadataFromFields(classType, contextMetadata) {
200
201
  }));
201
202
  // Get getter methods (for @calculatedField)
202
203
  const methods = getAllGetters(classType, contextMetadata);
204
+ // Check for @returns decorated handle method
205
+ const handleReturnType = (0, returns_1.getReturnTypeMetadata)(contextMetadata, 'handle');
206
+ if (handleReturnType) {
207
+ // Add handle method with its return type to methods array
208
+ methods.push({
209
+ name: 'handle',
210
+ typeInfo: handleReturnType,
211
+ dependencies: [],
212
+ });
213
+ }
203
214
  return {
204
215
  name: classType.name,
205
216
  type: classType,
@@ -10,6 +10,7 @@ export * from './global-error-handler';
10
10
  export * from './notification';
11
11
  export * from './projects';
12
12
  export * from './read-model';
13
+ export * from './returns';
13
14
  export * from './role';
14
15
  export * from './scheduled-command';
15
16
  export * from './schema-migration';
@@ -12,6 +12,7 @@ tslib_1.__exportStar(require("./global-error-handler"), exports);
12
12
  tslib_1.__exportStar(require("./notification"), exports);
13
13
  tslib_1.__exportStar(require("./projects"), exports);
14
14
  tslib_1.__exportStar(require("./read-model"), exports);
15
+ tslib_1.__exportStar(require("./returns"), exports);
15
16
  tslib_1.__exportStar(require("./role"), exports);
16
17
  tslib_1.__exportStar(require("./scheduled-command"), exports);
17
18
  tslib_1.__exportStar(require("./schema-migration"), exports);
@@ -0,0 +1,37 @@
1
+ import { TypeFunction, TypeMetadata } from '@magek/common';
2
+ import { MethodDecoratorContext, DecoratorMetadataObject } from './decorator-types';
3
+ /** Symbol used to store return type metadata in decorator context.metadata */
4
+ export declare const RETURNS_METADATA_KEY: unique symbol;
5
+ /** Metadata stored for each method's return type */
6
+ export interface ReturnsMetadata {
7
+ methodName: string;
8
+ typeFunction: TypeFunction;
9
+ }
10
+ /**
11
+ * Get the return type metadata for a method from decorator metadata.
12
+ *
13
+ * @param contextMetadata - The context.metadata from a class decorator
14
+ * @param methodName - The name of the method to get return type for
15
+ */
16
+ export declare function getReturnTypeMetadata(contextMetadata: DecoratorMetadataObject | undefined, methodName: string): TypeMetadata | undefined;
17
+ /**
18
+ * @returns() decorator for explicit return type declaration on methods.
19
+ *
20
+ * Uses TC39 Stage 3 decorators to capture return type metadata that is used
21
+ * for GraphQL schema generation. The type specified should be the GraphQL
22
+ * return type (not wrapped in Promise).
23
+ *
24
+ * Usage:
25
+ * @returns(type => UUID)
26
+ * public static async handle(...): Promise<UUID> { ... }
27
+ *
28
+ * @returns(type => String)
29
+ * public static async handle(...): Promise<string> { ... }
30
+ *
31
+ * @returns(type => [CartItem])
32
+ * public static async handle(...): Promise<CartItem[]> { ... }
33
+ */
34
+ export declare function returns(typeFunction: TypeFunction): MethodDecorator;
35
+ /** Type for the returns decorator */
36
+ type MethodDecorator = (target: unknown, context: MethodDecoratorContext) => void;
37
+ export {};
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RETURNS_METADATA_KEY = void 0;
4
+ exports.getReturnTypeMetadata = getReturnTypeMetadata;
5
+ exports.returns = returns;
6
+ /** Symbol used to store return type metadata in decorator context.metadata */
7
+ exports.RETURNS_METADATA_KEY = Symbol.for('magek:returns');
8
+ /**
9
+ * Analyze a type and convert it to TypeMetadata for return types.
10
+ * This returns the GraphQL-compatible type directly (not wrapped in Promise).
11
+ */
12
+ function analyzeReturnType(targetType) {
13
+ // Handle primitives
14
+ if (targetType === String) {
15
+ return {
16
+ name: 'string',
17
+ typeGroup: 'String',
18
+ typeName: 'String',
19
+ parameters: [],
20
+ isNullable: false,
21
+ isGetAccessor: false,
22
+ type: String,
23
+ };
24
+ }
25
+ if (targetType === Number) {
26
+ return {
27
+ name: 'number',
28
+ typeGroup: 'Number',
29
+ typeName: 'Number',
30
+ parameters: [],
31
+ isNullable: false,
32
+ isGetAccessor: false,
33
+ type: Number,
34
+ };
35
+ }
36
+ if (targetType === Boolean) {
37
+ return {
38
+ name: 'boolean',
39
+ typeGroup: 'Boolean',
40
+ typeName: 'Boolean',
41
+ parameters: [],
42
+ isNullable: false,
43
+ isGetAccessor: false,
44
+ type: Boolean,
45
+ };
46
+ }
47
+ // Handle void - return never to signal void return
48
+ if (targetType === undefined || targetType === null) {
49
+ return {
50
+ name: 'never',
51
+ typeGroup: 'Other',
52
+ typeName: 'never',
53
+ parameters: [],
54
+ isNullable: false,
55
+ isGetAccessor: false,
56
+ };
57
+ }
58
+ // Handle Array
59
+ if (Array.isArray(targetType)) {
60
+ if (targetType.length === 0) {
61
+ throw new Error('@returns decorator array type must specify an element type, e.g., @returns(type => [String])');
62
+ }
63
+ const elementType = targetType[0];
64
+ const elementMetadata = analyzeReturnType(elementType);
65
+ return {
66
+ name: `${elementMetadata.name}[]`,
67
+ typeGroup: 'Array',
68
+ typeName: 'Array',
69
+ parameters: [elementMetadata],
70
+ isNullable: false,
71
+ isGetAccessor: false,
72
+ };
73
+ }
74
+ // Handle Class types (e.g., UUID, custom types)
75
+ if (typeof targetType === 'function') {
76
+ return {
77
+ name: targetType.name || 'Unknown',
78
+ typeGroup: 'Class',
79
+ typeName: targetType.name,
80
+ parameters: [],
81
+ isNullable: false,
82
+ isGetAccessor: false,
83
+ type: targetType,
84
+ };
85
+ }
86
+ // Default fallback
87
+ return {
88
+ name: 'unknown',
89
+ typeGroup: 'Other',
90
+ typeName: 'unknown',
91
+ parameters: [],
92
+ isNullable: false,
93
+ isGetAccessor: false,
94
+ };
95
+ }
96
+ /**
97
+ * Get the return type metadata for a method from decorator metadata.
98
+ *
99
+ * @param contextMetadata - The context.metadata from a class decorator
100
+ * @param methodName - The name of the method to get return type for
101
+ */
102
+ function getReturnTypeMetadata(contextMetadata, methodName) {
103
+ if (!contextMetadata) {
104
+ return undefined;
105
+ }
106
+ const returnsMetadataList = contextMetadata[exports.RETURNS_METADATA_KEY];
107
+ if (!returnsMetadataList) {
108
+ return undefined;
109
+ }
110
+ const metadata = returnsMetadataList.find((m) => m.methodName === methodName);
111
+ if (!metadata) {
112
+ return undefined;
113
+ }
114
+ // Evaluate the type function and analyze the result
115
+ const typeResult = metadata.typeFunction();
116
+ // Return the actual type (not wrapped in Promise) - this is the GraphQL return type
117
+ return analyzeReturnType(typeResult);
118
+ }
119
+ /**
120
+ * @returns() decorator for explicit return type declaration on methods.
121
+ *
122
+ * Uses TC39 Stage 3 decorators to capture return type metadata that is used
123
+ * for GraphQL schema generation. The type specified should be the GraphQL
124
+ * return type (not wrapped in Promise).
125
+ *
126
+ * Usage:
127
+ * @returns(type => UUID)
128
+ * public static async handle(...): Promise<UUID> { ... }
129
+ *
130
+ * @returns(type => String)
131
+ * public static async handle(...): Promise<string> { ... }
132
+ *
133
+ * @returns(type => [CartItem])
134
+ * public static async handle(...): Promise<CartItem[]> { ... }
135
+ */
136
+ function returns(typeFunction) {
137
+ return function returnsDecorator(_target, context) {
138
+ const methodName = context.name.toString();
139
+ // Store in context.metadata for later retrieval by class decorators
140
+ if (context.metadata) {
141
+ if (!context.metadata[exports.RETURNS_METADATA_KEY]) {
142
+ context.metadata[exports.RETURNS_METADATA_KEY] = [];
143
+ }
144
+ const returnsList = context.metadata[exports.RETURNS_METADATA_KEY];
145
+ // Remove any existing entry for this method (in case of decorator re-application)
146
+ const filteredList = returnsList.filter((m) => m.methodName !== methodName);
147
+ filteredList.push({
148
+ methodName,
149
+ typeFunction,
150
+ });
151
+ context.metadata[exports.RETURNS_METADATA_KEY] = filteredList;
152
+ }
153
+ };
154
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MagekEventProcessor = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@magek/common");
6
+ const promises_1 = require("./utils/promises");
6
7
  const register_handler_1 = require("./register-handler");
7
8
  const global_error_dispatcher_1 = require("./global-error-dispatcher");
8
9
  const instrumentation_1 = require("./instrumentation");
@@ -33,7 +34,7 @@ let MagekEventProcessor = (() => {
33
34
  if (!(entityName in config.topicToEvent)) {
34
35
  eventEnvelopesProcessors.push(MagekEventProcessor.snapshotAndUpdateReadModels(config, entityName, entityID, eventStore, readModelStore));
35
36
  }
36
- await common_1.Promises.allSettledAndFulfilled(eventEnvelopesProcessors);
37
+ await promises_1.Promises.allSettledAndFulfilled(eventEnvelopesProcessors);
37
38
  };
38
39
  }
39
40
  static async filterDispatched(config, eventEnvelopes, eventStore) {
@@ -66,7 +67,7 @@ let MagekEventProcessor = (() => {
66
67
  static async dispatchEntityEventsToEventHandlers(entityEventEnvelopes, config) {
67
68
  const logger = (0, common_1.getLogger)(config, 'MagekEventDispatcher.dispatchEntityEventsToEventHandlers');
68
69
  try {
69
- await common_1.Promises.allSettledAndFulfilled(entityEventEnvelopes.map(async (eventEnvelope) => {
70
+ await promises_1.Promises.allSettledAndFulfilled(entityEventEnvelopes.map(async (eventEnvelope) => {
70
71
  let eventHandlers = config.eventHandlers[eventEnvelope.typeName] || [];
71
72
  const globalEventHandler = config.eventHandlers[decorators_1.GLOBAL_EVENT_HANDLERS];
72
73
  if (globalEventHandler && globalEventHandler.length > 0) {
@@ -79,13 +80,13 @@ let MagekEventProcessor = (() => {
79
80
  const eventInstance = this.getEventInstance(config, eventEnvelope);
80
81
  if (eventHandlers && eventHandlers.length > 0) {
81
82
  try {
82
- await common_1.Promises.allSettledAndFulfilled(eventHandlers.map(async (eventHandler) => {
83
+ await promises_1.Promises.allSettledAndFulfilled(eventHandlers.map(async (eventHandler) => {
83
84
  logger.debug('Calling "handle" method on event handler: ', eventHandler);
84
85
  await this.callEventHandler(eventHandler, eventInstance, eventEnvelope, config);
85
86
  }));
86
87
  }
87
88
  catch (error) {
88
- if (error instanceof common_1.PromisesError) {
89
+ if (error instanceof promises_1.PromisesError) {
89
90
  logger.error(`Failed to process handlers for event ${eventEnvelope.typeName}:`, error.failedReasons);
90
91
  }
91
92
  else {
@@ -96,7 +97,7 @@ let MagekEventProcessor = (() => {
96
97
  }));
97
98
  }
98
99
  catch (error) {
99
- if (error instanceof common_1.PromisesError) {
100
+ if (error instanceof promises_1.PromisesError) {
100
101
  logger.error('Failed to process events:', error.failedReasons);
101
102
  }
102
103
  else {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ReadModelStore = void 0;
4
4
  const common_1 = require("@magek/common");
5
+ const promises_1 = require("../utils/promises");
5
6
  const global_error_dispatcher_1 = require("../global-error-dispatcher");
6
7
  const read_model_searcher_1 = require("./read-model-searcher");
7
8
  const read_model_schema_migrator_1 = require("../read-model-schema-migrator");
@@ -28,7 +29,7 @@ class ReadModelStore {
28
29
  const sequenceKey = this.sequenceKeyForProjection(entityInstance, projectionMetadata);
29
30
  return this.projectEntity(entityInstance, projectionMetadata, entityMetadata, entitySnapshotEnvelope, readModelName, deleteEvent, sequenceKey);
30
31
  });
31
- await common_1.Promises.allSettledAndFulfilled(projectReadModelPromises);
32
+ await promises_1.Promises.allSettledAndFulfilled(projectReadModelPromises);
32
33
  }
33
34
  /**
34
35
  * Gets the read models for a given entity instance using the projection metadata
@@ -123,10 +124,10 @@ class ReadModelStore {
123
124
  const newProjections = await this.projectionsForReadModels(entitySnapshotEnvelope, readModelName, sequenceKey, projectionMetadata, entityInstance, entityMetadata, deleteEvent, currentReadModel);
124
125
  existingReadModelsProjections.push(...newProjections);
125
126
  }
126
- return common_1.Promises.allSettledAndFulfilled(existingReadModelsProjections);
127
+ return promises_1.Promises.allSettledAndFulfilled(existingReadModelsProjections);
127
128
  }
128
129
  const newProjections = await this.projectionsForReadModels(entitySnapshotEnvelope, readModelName, sequenceKey, projectionMetadata, entityInstance, entityMetadata, deleteEvent);
129
- return common_1.Promises.allSettledAndFulfilled(newProjections);
130
+ return promises_1.Promises.allSettledAndFulfilled(newProjections);
130
131
  }
131
132
  async projectionsForReadModels(entitySnapshotEnvelope, readModelName, sequenceKey, projectionMetadata, entityInstance, entityMetadata, deleteEvent, currentReadModel) {
132
133
  const projections = [];
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MagekSubscribersNotifier = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@magek/common");
6
+ const promises_1 = require("./utils/promises");
6
7
  const graphql = require("graphql");
7
8
  const graphql_generator_1 = require("./services/graphql/graphql-generator");
8
9
  const read_model_pub_sub_1 = require("./services/pub-sub/read-model-pub-sub");
@@ -32,7 +33,7 @@ let MagekSubscribersNotifier = (() => {
32
33
  const subscriptions = await this.getSubscriptions(readModelEnvelopes);
33
34
  logger.debug('Found the following subscriptions for those read models: ', subscriptions);
34
35
  const pubSub = this.getPubSub(readModelEnvelopes);
35
- await common_1.Promises.allSettledAndFulfilled(subscriptions.map(this.runSubscriptionAndNotify.bind(this, pubSub)));
36
+ await promises_1.Promises.allSettledAndFulfilled(subscriptions.map(this.runSubscriptionAndNotify.bind(this, pubSub)));
36
37
  }
37
38
  catch (e) {
38
39
  logger.error(e);
@@ -0,0 +1,25 @@
1
+ export declare class Promises {
2
+ /**
3
+ * Waits until all the passed promise-like values are settled, no matter if they were fulfilled or rejected.
4
+ * If some rejected were found, an array with all the rejected promises is thrown.
5
+ * If all were fulfilled, an array of PromiseFulfilledResult is returned
6
+ * @param values Array of promise-like values to be wait for
7
+ * @throws an array of PromiseRejectedResult with all the rejected promises, if any
8
+ *
9
+ * Comparison with other similar Promise methods:
10
+ * - `Promise.all`: This has an "all-or-nothing" behavior. As long as one of the promises is rejected, the result is
11
+ * rejected. More importantly, **it does not wait for al the promises to finish**, which could lead to undesired behaviors
12
+ * - `Promise.allSettled`: This method waits for all the promises to finish and then returns an array of results. Some
13
+ * of them will be fulfilled and some rejected. More importantly, **it never throws an error**, which could lead to
14
+ * unexpected consequences. For example if you do "await Promise.allSettle(...)" expecting it to throw if some of them
15
+ * failed, you won't get that.
16
+ *
17
+ * In brief, `Promises.allSettledAndFulfilled` behaves exactly the same way as `Promise.allSettle` but it throws with
18
+ * an array of the failed promises, only if there are any.
19
+ */
20
+ static allSettledAndFulfilled<TValue>(values: Iterable<TValue>): ReturnType<PromiseConstructor['allSettled']>;
21
+ }
22
+ export declare class PromisesError extends Error {
23
+ readonly failedReasons: Array<unknown>;
24
+ constructor(rejectedResults: Array<PromiseRejectedResult>);
25
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PromisesError = exports.Promises = void 0;
4
+ class Promises {
5
+ /**
6
+ * Waits until all the passed promise-like values are settled, no matter if they were fulfilled or rejected.
7
+ * If some rejected were found, an array with all the rejected promises is thrown.
8
+ * If all were fulfilled, an array of PromiseFulfilledResult is returned
9
+ * @param values Array of promise-like values to be wait for
10
+ * @throws an array of PromiseRejectedResult with all the rejected promises, if any
11
+ *
12
+ * Comparison with other similar Promise methods:
13
+ * - `Promise.all`: This has an "all-or-nothing" behavior. As long as one of the promises is rejected, the result is
14
+ * rejected. More importantly, **it does not wait for al the promises to finish**, which could lead to undesired behaviors
15
+ * - `Promise.allSettled`: This method waits for all the promises to finish and then returns an array of results. Some
16
+ * of them will be fulfilled and some rejected. More importantly, **it never throws an error**, which could lead to
17
+ * unexpected consequences. For example if you do "await Promise.allSettle(...)" expecting it to throw if some of them
18
+ * failed, you won't get that.
19
+ *
20
+ * In brief, `Promises.allSettledAndFulfilled` behaves exactly the same way as `Promise.allSettle` but it throws with
21
+ * an array of the failed promises, only if there are any.
22
+ */
23
+ static async allSettledAndFulfilled(values) {
24
+ const results = await Promise.allSettled(values); // Promise.allSettled never throws
25
+ // Get all the failed promises
26
+ const failed = results.filter((result) => result.status === 'rejected');
27
+ // Throw if we found any failed ones
28
+ if (failed.length > 0) {
29
+ throw new PromisesError(failed);
30
+ }
31
+ return results;
32
+ }
33
+ }
34
+ exports.Promises = Promises;
35
+ class PromisesError extends Error {
36
+ failedReasons;
37
+ constructor(rejectedResults) {
38
+ const reasons = rejectedResults.map((res) => res.reason);
39
+ super(reasons.join('. '));
40
+ this.failedReasons = reasons;
41
+ }
42
+ }
43
+ exports.PromisesError = PromisesError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magek/core",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Library for your Magek apps",
5
5
  "author": "Boosterin Labs SLU",
6
6
  "homepage": "https://magek.ai",
@@ -23,7 +23,7 @@
23
23
  "node": ">=22.0.0 <23.0.0"
24
24
  },
25
25
  "dependencies": {
26
- "@magek/common": "^0.0.7",
26
+ "@magek/common": "^0.0.9",
27
27
  "fp-ts": "2.16.11",
28
28
  "graphql-scalars": "1.25.0",
29
29
  "inflected": "2.1.0",
@@ -35,7 +35,7 @@
35
35
  "fast-check": "4.5.3"
36
36
  },
37
37
  "devDependencies": {
38
- "@magek/eslint-config": "^0.0.7",
38
+ "@magek/eslint-config": "^0.0.9",
39
39
  "@types/chai": "5.2.3",
40
40
  "@types/chai-as-promised": "8.0.2",
41
41
  "@types/inflected": "2.1.3",