@causa/runtime-google 1.5.3 → 1.6.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.
Files changed (36) hide show
  1. package/README.md +11 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/pubsub/interceptor.d.ts +4 -3
  5. package/dist/pubsub/interceptor.js +6 -5
  6. package/dist/pubsub/testing.d.ts +45 -7
  7. package/dist/pubsub/testing.js +56 -21
  8. package/dist/scheduler/cloud-scheduler-info.d.ts +13 -0
  9. package/dist/scheduler/cloud-scheduler-info.js +36 -0
  10. package/dist/scheduler/index.d.ts +3 -0
  11. package/dist/scheduler/index.js +3 -0
  12. package/dist/scheduler/interceptor.d.ts +34 -0
  13. package/dist/scheduler/interceptor.js +98 -0
  14. package/dist/scheduler/scheduler-event-info.decorator.d.ts +14 -0
  15. package/dist/scheduler/scheduler-event-info.decorator.js +10 -0
  16. package/dist/scheduler/testing.d.ts +41 -0
  17. package/dist/scheduler/testing.js +48 -0
  18. package/dist/spanner/entity-manager.d.ts +8 -1
  19. package/dist/spanner/entity-manager.js +9 -2
  20. package/dist/spanner/error-converter.js +10 -3
  21. package/dist/spanner/errors.d.ts +1 -1
  22. package/dist/spanner/errors.js +2 -2
  23. package/dist/spanner/types.d.ts +6 -0
  24. package/dist/tasks/cloud-tasks-info.d.ts +35 -0
  25. package/dist/tasks/cloud-tasks-info.js +90 -0
  26. package/dist/tasks/index.d.ts +3 -0
  27. package/dist/tasks/index.js +3 -0
  28. package/dist/tasks/interceptor.d.ts +30 -0
  29. package/dist/tasks/interceptor.js +90 -0
  30. package/dist/tasks/task-event-info.decorator.d.ts +14 -0
  31. package/dist/tasks/task-event-info.decorator.js +8 -0
  32. package/dist/tasks/testing.d.ts +41 -0
  33. package/dist/tasks/testing.js +59 -0
  34. package/dist/testing.d.ts +2 -0
  35. package/dist/testing.js +6 -0
  36. package/package.json +14 -14
package/README.md CHANGED
@@ -82,12 +82,22 @@ The `PubSubPublisherModule` provides the `PubSub` client, as well as the `PubSub
82
82
 
83
83
  The `PubSubPublisher` requires the `PUBSUB_TOPIC_*` environment variables to be set for all the topics a service is expected to publish to. The name of the environment variables should be prefixed with `PUBSUB_TOPIC_`, followed by the topic full name in upper case, and using `_` as the only punctuation. For example, `my-domain.my-topic.v1` would become `PUBSUB_TOPIC_MY_DOMAIN_MY_TOPIC_V1`.
84
84
 
85
- For services being triggered by Pub/Sub messages, the `PubSubEventHandlerModule` can be used to automatically parse Pub/Sub messages coming from HTTP requests made by a Pub/Sub push subscription. Any route with a parameter decorated with `@EventBody` will trigger the `PubSubEventHandlerInterceptor`, and will receive the parsed event pushed by Pub/Sub.
85
+ For services being triggered by Pub/Sub messages, the `PubSubEventHandlerInterceptor` automatically parses Pub/Sub messages coming from HTTP requests made by a Pub/Sub push subscription. If the interceptor is set up at the application level, any route with a parameter decorated with `@EventBody` will trigger the interceptor, and will receive the parsed event pushed by Pub/Sub. The `PubSubEventHandlerInterceptor.withSerializer` method should be used to create the interceptor with the desired serializer. It also allows defining whether the interceptor `isDefault`, i.e. whether `@UseEventHandler` is required on route handlers to apply the interceptor.
86
86
 
87
87
  The `PubSubHealthIndicator` is a `HealthIndicator` which can be used in a health check controller, such as the `GoogleHealthcheckModule`. It attempts to list topics using the Pub/Sub client to check connectivity to the Pub/Sub API.
88
88
 
89
89
  To test publishers, the `PubSubFixture` handles the creation and deletion of temporary topics, and provides `expect*` utilities to check for published messages. To test event handlers, it also provides the `makeRequester()` utility, which returns a function that can be used to make HTTP requests in the same way a Pub/Sub push subscription would.
90
90
 
91
+ ### Cloud Tasks
92
+
93
+ The `CloudTasksEventHandlerInterceptor` handles HTTP requests triggered by Cloud Tasks. It parses task metadata from request headers into a `CloudTasksInfo` object, and validates the request body against the type decorated with `@EventBody`. The `@CloudTasksEventInfo` decorator can be used on a route handler parameter to retrieve the parsed `CloudTasksInfo`. The `CloudTasksEventHandlerInterceptor.withOptions` static method can be used to create the interceptor, defining whether it `isDefault`.
94
+
95
+ ### Cloud Scheduler
96
+
97
+ The `CloudSchedulerEventHandlerInterceptor` handles HTTP requests triggered by Cloud Scheduler jobs. It parses scheduler job metadata from request headers into a `CloudSchedulerInfo` object. The `@CloudSchedulerEventInfo` decorator can be used on a route handler parameter to retrieve the parsed `CloudSchedulerInfo`. The `CloudSchedulerEventHandlerInterceptor.withOptions` static method can be used to create the interceptor, defining whether it `isDefault`.
98
+
99
+ Because Cloud Scheduler jobs are often configured to not send a body, the `@EventBody` parameter can be typed as a plain `object`. In that case, body parsing and validation are skipped, and an empty object is returned.
100
+
91
101
  ### Spanner
92
102
 
93
103
  The `SpannerEntityManager` is an entity manager having some similarities with TypeORM, but with a much more limited feature set. It handles entity classes decorated using `@SpannerTable` and `@SpannerColumn`.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './firestore/index.js';
4
4
  export * from './identity-platform/index.js';
5
5
  export * from './logging/index.js';
6
6
  export * from './pubsub/index.js';
7
+ export * from './scheduler/index.js';
7
8
  export * from './spanner/index.js';
8
9
  export * from './tasks/index.js';
9
10
  export * from './transaction/index.js';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export * from './firestore/index.js';
4
4
  export * from './identity-platform/index.js';
5
5
  export * from './logging/index.js';
6
6
  export * from './pubsub/index.js';
7
+ export * from './scheduler/index.js';
7
8
  export * from './spanner/index.js';
8
9
  export * from './tasks/index.js';
9
10
  export * from './transaction/index.js';
@@ -1,5 +1,5 @@
1
1
  import { type ObjectSerializer } from '@causa/runtime';
2
- import { BaseEventHandlerInterceptor, Logger, type ParsedEventRequest } from '@causa/runtime/nestjs';
2
+ import { BaseEventHandlerInterceptor, type EventHandlerInterceptorOptions, Logger, type ParsedEventRequest } from '@causa/runtime/nestjs';
3
3
  import { type ExecutionContext, type Type } from '@nestjs/common';
4
4
  import { Reflector } from '@nestjs/core';
5
5
  import type { Request } from 'express';
@@ -34,7 +34,7 @@ declare class PubSubMessage {
34
34
  */
35
35
  export declare class PubSubEventHandlerInterceptor extends BaseEventHandlerInterceptor {
36
36
  protected readonly serializer: ObjectSerializer;
37
- constructor(serializer: ObjectSerializer, reflector: Reflector, logger: Logger);
37
+ constructor(serializer: ObjectSerializer, reflector: Reflector, logger: Logger, options?: EventHandlerInterceptorOptions);
38
38
  /**
39
39
  * Parses the given request as the payload of a Pub/Sub push request.
40
40
  *
@@ -53,8 +53,9 @@ export declare class PubSubEventHandlerInterceptor extends BaseEventHandlerInter
53
53
  * This can be used with the `UseInterceptors` decorator.
54
54
  *
55
55
  * @param serializer The {@link ObjectSerializer} to use to deserialize the event data.
56
+ * @param options Options for the interceptor.
56
57
  * @returns A class that can be used as an interceptor for Pub/Sub event handlers.
57
58
  */
58
- static withSerializer(serializer: ObjectSerializer): Type<PubSubEventHandlerInterceptor>;
59
+ static withSerializer(serializer: ObjectSerializer, options?: EventHandlerInterceptorOptions): Type<PubSubEventHandlerInterceptor>;
59
60
  }
60
61
  export {};
@@ -82,8 +82,8 @@ __decorate([
82
82
  */
83
83
  let PubSubEventHandlerInterceptor = PubSubEventHandlerInterceptor_1 = class PubSubEventHandlerInterceptor extends BaseEventHandlerInterceptor {
84
84
  serializer;
85
- constructor(serializer, reflector, logger) {
86
- super(PUBSUB_EVENT_HANDLER_ID, reflector, logger);
85
+ constructor(serializer, reflector, logger, options = {}) {
86
+ super(PUBSUB_EVENT_HANDLER_ID, reflector, logger, options);
87
87
  this.serializer = serializer;
88
88
  this.logger.setContext(PubSubEventHandlerInterceptor_1.name);
89
89
  }
@@ -138,12 +138,13 @@ let PubSubEventHandlerInterceptor = PubSubEventHandlerInterceptor_1 = class PubS
138
138
  * This can be used with the `UseInterceptors` decorator.
139
139
  *
140
140
  * @param serializer The {@link ObjectSerializer} to use to deserialize the event data.
141
+ * @param options Options for the interceptor.
141
142
  * @returns A class that can be used as an interceptor for Pub/Sub event handlers.
142
143
  */
143
- static withSerializer(serializer) {
144
+ static withSerializer(serializer, options = {}) {
144
145
  let PubSubEventHandlerInterceptorWithSerializer = class PubSubEventHandlerInterceptorWithSerializer extends PubSubEventHandlerInterceptor_1 {
145
146
  constructor(reflector, logger) {
146
- super(serializer, reflector, logger);
147
+ super(serializer, reflector, logger, options);
147
148
  }
148
149
  };
149
150
  PubSubEventHandlerInterceptorWithSerializer = __decorate([
@@ -156,6 +157,6 @@ let PubSubEventHandlerInterceptor = PubSubEventHandlerInterceptor_1 = class PubS
156
157
  PubSubEventHandlerInterceptor = PubSubEventHandlerInterceptor_1 = __decorate([
157
158
  Injectable(),
158
159
  __metadata("design:paramtypes", [Object, Reflector,
159
- Logger])
160
+ Logger, Object])
160
161
  ], PubSubEventHandlerInterceptor);
161
162
  export { PubSubEventHandlerInterceptor };
@@ -2,6 +2,7 @@ import { type ObjectSerializer } from '@causa/runtime';
2
2
  import type { AppFixture, EventFixture, Fixture, NestJsModuleOverrider } from '@causa/runtime/nestjs/testing';
3
3
  import { PubSub, Subscription, Topic } from '@google-cloud/pubsub';
4
4
  import { type Type } from '@nestjs/common';
5
+ import 'jest-extended';
5
6
  /**
6
7
  * A received Pub/Sub message that was deserialized.
7
8
  */
@@ -28,6 +29,22 @@ export type ExpectMessageOptions = {
28
29
  * Defaults to `2000`.
29
30
  */
30
31
  timeout?: number;
32
+ /**
33
+ * When `true`, the received messages must exactly match the expected messages (same members), rather than merely
34
+ * including all of them.
35
+ * Defaults to `false`.
36
+ */
37
+ exact?: boolean;
38
+ };
39
+ /**
40
+ * Options for the {@link PubSubFixture.expectEvent} method.
41
+ */
42
+ export type ExpectEventOptions = ExpectMessageOptions & {
43
+ /**
44
+ * The attributes expected to have been published with the event.
45
+ * This may contain only a subset of the attributes.
46
+ */
47
+ attributes?: Record<string, string>;
31
48
  };
32
49
  /**
33
50
  * Options when making a request to an endpoint handling Pub/Sub events using an {@link EventRequester}.
@@ -124,6 +141,23 @@ export declare class PubSubFixture implements Fixture, EventFixture {
124
141
  */
125
142
  expectedStatus?: number;
126
143
  }): EventRequester;
144
+ /**
145
+ * Gets the received messages for the specified topic.
146
+ *
147
+ * @param topic The original name of the event topic.
148
+ * @returns The received messages.
149
+ */
150
+ getReceivedMessages(topic: string): ReceivedPubSubEvent[];
151
+ /**
152
+ * Checks that the given messages have been published to the specified topic.
153
+ * Each expected message must match a distinct received message.
154
+ *
155
+ * @param topic The original name of the event topic.
156
+ * @param expectedMessages The messages expected to have been published.
157
+ * Each can be an `expect` expression, e.g. `expect.objectContaining({})`.
158
+ * @param options Options for the expectation.
159
+ */
160
+ expectMessages(topic: string, expectedMessages: Partial<ReceivedPubSubEvent>[], options?: ExpectMessageOptions): Promise<void>;
127
161
  /**
128
162
  * Checks that the given message has been published to the specified topic.
129
163
  *
@@ -133,6 +167,16 @@ export declare class PubSubFixture implements Fixture, EventFixture {
133
167
  * @param options Options for the expectation.
134
168
  */
135
169
  expectMessage(topic: string, expectedMessage: any, options?: ExpectMessageOptions): Promise<void>;
170
+ /**
171
+ * Uses {@link PubSubFixture.expectMessages} to check that the given events have been published to the specified
172
+ * topic. Each element in `expectedEvents` is the payload of a message, i.e. the `event` property.
173
+ * If `attributes` are provided, all events are expected to share those attributes.
174
+ *
175
+ * @param topic The original name of the event topic.
176
+ * @param expectedEvents The events expected to have been published.
177
+ * @param options Options for the expectation.
178
+ */
179
+ expectEvents(topic: string, expectedEvents: any[], options?: ExpectEventOptions): Promise<void>;
136
180
  /**
137
181
  * Uses {@link PubSubFixture.expectMessage} to check that the given event has been published to the specified
138
182
  * topic. The `expectedEvent` is the payload of the message, i.e. the `event` property.
@@ -141,13 +185,7 @@ export declare class PubSubFixture implements Fixture, EventFixture {
141
185
  * @param expectedEvent The event expected to have been published.
142
186
  * @param options Options for the expectation.
143
187
  */
144
- expectEvent(topic: string, expectedEvent: any, options?: ExpectMessageOptions & {
145
- /**
146
- * The attributes expected to have been published with the event.
147
- * This may contain only a subset of the attributes.
148
- */
149
- attributes?: Record<string, string>;
150
- }): Promise<void>;
188
+ expectEvent(topic: string, expectedEvent: any, options?: ExpectEventOptions): Promise<void>;
151
189
  /**
152
190
  * Checks that no message has been published to the given topic.
153
191
  * By default, because publishing (and receiving) the messages is asynchronous, a small delay is added before checking
@@ -1,6 +1,7 @@
1
1
  import { JsonObjectSerializer } from '@causa/runtime';
2
2
  import { Message, PubSub, Subscription, Topic } from '@google-cloud/pubsub';
3
3
  import { HttpStatus } from '@nestjs/common';
4
+ import 'jest-extended';
4
5
  import { setTimeout } from 'timers/promises';
5
6
  import * as uuid from 'uuid';
6
7
  import { getConfigurationKeyForTopic } from './configuration.js';
@@ -143,30 +144,46 @@ export class PubSubFixture {
143
144
  };
144
145
  }
145
146
  /**
146
- * Checks that the given message has been published to the specified topic.
147
+ * Gets the received messages for the specified topic.
147
148
  *
148
149
  * @param topic The original name of the event topic.
149
- * @param expectedMessage The message expected to have been published.
150
- * This can be an `expect` expression, e.g. `expect.objectContaining({})`.
151
- * @param options Options for the expectation.
150
+ * @returns The received messages.
152
151
  */
153
- async expectMessage(topic, expectedMessage, options = {}) {
152
+ getReceivedMessages(topic) {
154
153
  const fixture = this.topics[topic];
155
154
  if (!fixture) {
156
155
  throw new Error(`Fixture for topic '${topic}' does not exist.`);
157
156
  }
158
- const timeoutTime = new Date().getTime() + (options.timeout ?? DEFAULT_EXPECT_TIMEOUT);
157
+ return fixture.messages;
158
+ }
159
+ /**
160
+ * Checks that the given messages have been published to the specified topic.
161
+ * Each expected message must match a distinct received message.
162
+ *
163
+ * @param topic The original name of the event topic.
164
+ * @param expectedMessages The messages expected to have been published.
165
+ * Each can be an `expect` expression, e.g. `expect.objectContaining({})`.
166
+ * @param options Options for the expectation.
167
+ */
168
+ async expectMessages(topic, expectedMessages, options = {}) {
169
+ const timeoutTime = Date.now() + (options.timeout ?? DEFAULT_EXPECT_TIMEOUT);
159
170
  while (true) {
171
+ const actualMessages = this.getReceivedMessages(topic);
160
172
  try {
161
- expect(fixture.messages).toContainEqual(expectedMessage);
173
+ if (options.exact) {
174
+ expect(actualMessages).toIncludeSameMembers(expectedMessages);
175
+ }
176
+ else {
177
+ expect(actualMessages).toIncludeAllMembers(expectedMessages);
178
+ }
162
179
  return;
163
180
  }
164
181
  catch (e) {
165
- if (new Date().getTime() >= timeoutTime) {
166
- if (fixture.messages.length === 1) {
167
- // This throws with a clearer message than `toContainEqual` because the single received message is actually
168
- // compared to the expected message.
169
- expect(fixture.messages[0]).toEqual(expectedMessage);
182
+ if (Date.now() >= timeoutTime) {
183
+ if (expectedMessages.length === 1 && actualMessages.length === 1) {
184
+ // This throws with a clearer message because the single received message is actually compared to the
185
+ // expected message.
186
+ expect(actualMessages[0]).toEqual(expectedMessages[0]);
170
187
  }
171
188
  throw e;
172
189
  }
@@ -174,6 +191,30 @@ export class PubSubFixture {
174
191
  }
175
192
  }
176
193
  }
194
+ /**
195
+ * Checks that the given message has been published to the specified topic.
196
+ *
197
+ * @param topic The original name of the event topic.
198
+ * @param expectedMessage The message expected to have been published.
199
+ * This can be an `expect` expression, e.g. `expect.objectContaining({})`.
200
+ * @param options Options for the expectation.
201
+ */
202
+ async expectMessage(topic, expectedMessage, options = {}) {
203
+ await this.expectMessages(topic, [expectedMessage], options);
204
+ }
205
+ /**
206
+ * Uses {@link PubSubFixture.expectMessages} to check that the given events have been published to the specified
207
+ * topic. Each element in `expectedEvents` is the payload of a message, i.e. the `event` property.
208
+ * If `attributes` are provided, all events are expected to share those attributes.
209
+ *
210
+ * @param topic The original name of the event topic.
211
+ * @param expectedEvents The events expected to have been published.
212
+ * @param options Options for the expectation.
213
+ */
214
+ async expectEvents(topic, expectedEvents, options = {}) {
215
+ const attributes = expect.objectContaining(options.attributes ?? {});
216
+ await this.expectMessages(topic, expectedEvents.map((event) => expect.objectContaining({ event, attributes })), options);
217
+ }
177
218
  /**
178
219
  * Uses {@link PubSubFixture.expectMessage} to check that the given event has been published to the specified
179
220
  * topic. The `expectedEvent` is the payload of the message, i.e. the `event` property.
@@ -183,10 +224,7 @@ export class PubSubFixture {
183
224
  * @param options Options for the expectation.
184
225
  */
185
226
  async expectEvent(topic, expectedEvent, options = {}) {
186
- await this.expectMessage(topic, expect.objectContaining({
187
- event: expectedEvent,
188
- attributes: expect.objectContaining(options.attributes ?? {}),
189
- }), options);
227
+ await this.expectEvents(topic, [expectedEvent], options);
190
228
  }
191
229
  /**
192
230
  * Checks that no message has been published to the given topic.
@@ -197,15 +235,11 @@ export class PubSubFixture {
197
235
  * @param options Options for the expectation.
198
236
  */
199
237
  async expectNoMessage(topic, options = {}) {
200
- const fixture = this.topics[topic];
201
- if (!fixture) {
202
- throw new Error(`Fixture for topic '${topic}' does not exist.`);
203
- }
204
238
  const delay = options.delay ?? DEFAULT_EXPECT_NO_MESSAGE_DELAY;
205
239
  if (delay > 0) {
206
240
  await setTimeout(delay);
207
241
  }
208
- const numMessages = fixture.messages.length;
242
+ const numMessages = this.getReceivedMessages(topic).length;
209
243
  if (numMessages > 0) {
210
244
  throw new Error(`Expected 0 messages in '${topic}' but found ${numMessages}.`);
211
245
  }
@@ -236,5 +270,6 @@ export class PubSubFixture {
236
270
  async delete() {
237
271
  await Promise.all(Object.keys(this.topics).map((t) => this.deleteTopic(t)));
238
272
  await this.pubSub.close();
273
+ this.appFixture = undefined;
239
274
  }
240
275
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Information about a Cloud Scheduler job, extracted from HTTP headers.
3
+ */
4
+ export declare class CloudSchedulerInfo {
5
+ /**
6
+ * The name of the Cloud Scheduler job.
7
+ */
8
+ readonly jobName: string;
9
+ /**
10
+ * The scheduled time for the job execution.
11
+ */
12
+ readonly scheduleTime?: Date;
13
+ }
@@ -0,0 +1,36 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { AllowMissing, IsDateType } from '@causa/runtime';
11
+ import { Expose } from 'class-transformer';
12
+ import { IsString } from 'class-validator';
13
+ /**
14
+ * Information about a Cloud Scheduler job, extracted from HTTP headers.
15
+ */
16
+ export class CloudSchedulerInfo {
17
+ /**
18
+ * The name of the Cloud Scheduler job.
19
+ */
20
+ jobName;
21
+ /**
22
+ * The scheduled time for the job execution.
23
+ */
24
+ scheduleTime;
25
+ }
26
+ __decorate([
27
+ Expose({ name: 'x-cloudscheduler-jobname' }),
28
+ IsString(),
29
+ __metadata("design:type", String)
30
+ ], CloudSchedulerInfo.prototype, "jobName", void 0);
31
+ __decorate([
32
+ Expose({ name: 'x-cloudscheduler-scheduletime' }),
33
+ IsDateType(),
34
+ AllowMissing(),
35
+ __metadata("design:type", Date)
36
+ ], CloudSchedulerInfo.prototype, "scheduleTime", void 0);
@@ -0,0 +1,3 @@
1
+ export { CloudSchedulerInfo } from './cloud-scheduler-info.js';
2
+ export { CLOUD_SCHEDULER_EVENT_HANDLER_ID, CloudSchedulerEventHandlerInterceptor, } from './interceptor.js';
3
+ export { CloudSchedulerEventInfo } from './scheduler-event-info.decorator.js';
@@ -0,0 +1,3 @@
1
+ export { CloudSchedulerInfo } from './cloud-scheduler-info.js';
2
+ export { CLOUD_SCHEDULER_EVENT_HANDLER_ID, CloudSchedulerEventHandlerInterceptor, } from './interceptor.js';
3
+ export { CloudSchedulerEventInfo } from './scheduler-event-info.decorator.js';
@@ -0,0 +1,34 @@
1
+ import { BaseEventHandlerInterceptor, type EventHandlerInterceptorOptions, Logger, type ParsedEventRequest } from '@causa/runtime/nestjs';
2
+ import { type ExecutionContext, type Type } from '@nestjs/common';
3
+ import { Reflector } from '@nestjs/core';
4
+ import type { Request } from 'express';
5
+ import { CloudSchedulerInfo } from './cloud-scheduler-info.js';
6
+ /**
7
+ * The ID of the Cloud Scheduler event handler interceptor, that can be passed to the `UseEventHandler` decorator.
8
+ */
9
+ export declare const CLOUD_SCHEDULER_EVENT_HANDLER_ID = "google.cloudScheduler";
10
+ /**
11
+ * The interceptor that should be added to controllers handling Cloud Scheduler events.
12
+ *
13
+ * Because Cloud Scheduler jobs are often configured to not send a body, the `@EventBody` parameter of route handlers
14
+ * can be typed as a plain `object`. In that case, body parsing and validation are skipped, and an empty object is
15
+ * returned.
16
+ */
17
+ export declare class CloudSchedulerEventHandlerInterceptor extends BaseEventHandlerInterceptor {
18
+ constructor(reflector: Reflector, logger: Logger, options?: EventHandlerInterceptorOptions);
19
+ /**
20
+ * Parses the Cloud Scheduler request, extracting job information from headers.
21
+ *
22
+ * @param request The express request object.
23
+ * @returns The parsed Cloud Scheduler information.
24
+ */
25
+ protected parseCloudSchedulerRequest(request: Request): Promise<CloudSchedulerInfo>;
26
+ protected parseEventFromContext(context: ExecutionContext, dataType: Type): Promise<ParsedEventRequest>;
27
+ /**
28
+ * Creates a Cloud Scheduler event handler interceptor class with the given options.
29
+ *
30
+ * @param options Options for the interceptor.
31
+ * @returns The Cloud Scheduler event handler interceptor class.
32
+ */
33
+ static withOptions(options: EventHandlerInterceptorOptions): Type<CloudSchedulerEventHandlerInterceptor>;
34
+ }
@@ -0,0 +1,98 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var CloudSchedulerEventHandlerInterceptor_1;
11
+ import { ValidationError, parseObject, validatorOptions } from '@causa/runtime';
12
+ import { BadRequestErrorDto, BaseEventHandlerInterceptor, Logger, throwHttpErrorResponse, } from '@causa/runtime/nestjs';
13
+ import { Injectable } from '@nestjs/common';
14
+ import { Reflector } from '@nestjs/core';
15
+ import { CloudSchedulerInfo } from './cloud-scheduler-info.js';
16
+ /**
17
+ * The ID of the Cloud Scheduler event handler interceptor, that can be passed to the `UseEventHandler` decorator.
18
+ */
19
+ export const CLOUD_SCHEDULER_EVENT_HANDLER_ID = 'google.cloudScheduler';
20
+ /**
21
+ * The interceptor that should be added to controllers handling Cloud Scheduler events.
22
+ *
23
+ * Because Cloud Scheduler jobs are often configured to not send a body, the `@EventBody` parameter of route handlers
24
+ * can be typed as a plain `object`. In that case, body parsing and validation are skipped, and an empty object is
25
+ * returned.
26
+ */
27
+ let CloudSchedulerEventHandlerInterceptor = CloudSchedulerEventHandlerInterceptor_1 = class CloudSchedulerEventHandlerInterceptor extends BaseEventHandlerInterceptor {
28
+ constructor(reflector, logger, options = {}) {
29
+ super(CLOUD_SCHEDULER_EVENT_HANDLER_ID, reflector, logger, options);
30
+ this.logger.setContext(CloudSchedulerEventHandlerInterceptor_1.name);
31
+ }
32
+ /**
33
+ * Parses the Cloud Scheduler request, extracting job information from headers.
34
+ *
35
+ * @param request The express request object.
36
+ * @returns The parsed Cloud Scheduler information.
37
+ */
38
+ async parseCloudSchedulerRequest(request) {
39
+ try {
40
+ const info = await parseObject(CloudSchedulerInfo, request.headers, {
41
+ ...validatorOptions,
42
+ forbidNonWhitelisted: false,
43
+ });
44
+ this.logger.info('Successfully parsed Cloud Scheduler request.');
45
+ return info;
46
+ }
47
+ catch (error) {
48
+ this.logger.error({
49
+ error: error.stack,
50
+ ...(error instanceof ValidationError
51
+ ? { validationMessages: error.validationMessages }
52
+ : {}),
53
+ }, 'Received invalid Cloud Scheduler request.');
54
+ throwHttpErrorResponse(new BadRequestErrorDto());
55
+ }
56
+ }
57
+ async parseEventFromContext(context, dataType) {
58
+ const request = context
59
+ .switchToHttp()
60
+ .getRequest();
61
+ request.cloudSchedulerInfo = await this.parseCloudSchedulerRequest(request);
62
+ // This supports `@EventBody()` decorating a plain object type. This can be useful as bodies are often not needed
63
+ // for Cloud Scheduler jobs.
64
+ if (dataType === Object) {
65
+ return { attributes: {}, body: {} };
66
+ }
67
+ return await this.wrapParsing(async () => {
68
+ const body = await parseObject(dataType, request.body, {
69
+ forbidNonWhitelisted: false,
70
+ });
71
+ return { attributes: {}, body };
72
+ });
73
+ }
74
+ /**
75
+ * Creates a Cloud Scheduler event handler interceptor class with the given options.
76
+ *
77
+ * @param options Options for the interceptor.
78
+ * @returns The Cloud Scheduler event handler interceptor class.
79
+ */
80
+ static withOptions(options) {
81
+ let CloudSchedulerEventHandlerInterceptorWithOptions = class CloudSchedulerEventHandlerInterceptorWithOptions extends CloudSchedulerEventHandlerInterceptor_1 {
82
+ constructor(reflector, logger) {
83
+ super(reflector, logger, options);
84
+ }
85
+ };
86
+ CloudSchedulerEventHandlerInterceptorWithOptions = __decorate([
87
+ Injectable(),
88
+ __metadata("design:paramtypes", [Reflector, Logger])
89
+ ], CloudSchedulerEventHandlerInterceptorWithOptions);
90
+ return CloudSchedulerEventHandlerInterceptorWithOptions;
91
+ }
92
+ };
93
+ CloudSchedulerEventHandlerInterceptor = CloudSchedulerEventHandlerInterceptor_1 = __decorate([
94
+ Injectable(),
95
+ __metadata("design:paramtypes", [Reflector,
96
+ Logger, Object])
97
+ ], CloudSchedulerEventHandlerInterceptor);
98
+ export { CloudSchedulerEventHandlerInterceptor };
@@ -0,0 +1,14 @@
1
+ import type { CloudSchedulerInfo } from './cloud-scheduler-info.js';
2
+ /**
3
+ * Additional information expected to be present on an express request that was parsed as a Cloud Scheduler request.
4
+ */
5
+ export type RequestWithCloudSchedulerInfo = {
6
+ /**
7
+ * Information about the Cloud Scheduler job.
8
+ */
9
+ cloudSchedulerInfo: CloudSchedulerInfo;
10
+ };
11
+ /**
12
+ * Decorates a route handler's parameter to populate it with information about the Cloud Scheduler job.
13
+ */
14
+ export declare const CloudSchedulerEventInfo: (...dataOrPipes: unknown[]) => ParameterDecorator;
@@ -0,0 +1,10 @@
1
+ import { createParamDecorator } from '@nestjs/common';
2
+ /**
3
+ * Decorates a route handler's parameter to populate it with information about the Cloud Scheduler job.
4
+ */
5
+ export const CloudSchedulerEventInfo = createParamDecorator((_data, ctx) => {
6
+ const request = ctx
7
+ .switchToHttp()
8
+ .getRequest();
9
+ return request.cloudSchedulerInfo;
10
+ });
@@ -0,0 +1,41 @@
1
+ import type { AppFixture, Fixture } from '@causa/runtime/nestjs/testing';
2
+ import type { CloudSchedulerInfo } from './cloud-scheduler-info.js';
3
+ /**
4
+ * Options when making a request to an endpoint handling Cloud Scheduler events using a {@link CloudSchedulerEventRequester}.
5
+ */
6
+ export type CloudSchedulerEventRequesterOptions = {
7
+ /**
8
+ * The expected status code when making the request.
9
+ * Default is `200`.
10
+ */
11
+ readonly expectedStatus?: number;
12
+ /**
13
+ * The information about the Cloud Scheduler job to include in the request headers.
14
+ * If not provided, default values will be used.
15
+ */
16
+ readonly jobInfo?: Partial<CloudSchedulerInfo>;
17
+ };
18
+ /**
19
+ * A function that makes a query to an endpoint handling Cloud Scheduler events and tests the response.
20
+ */
21
+ export type CloudSchedulerEventRequester = (event?: object, options?: CloudSchedulerEventRequesterOptions) => Promise<void>;
22
+ /**
23
+ * A utility class for testing Cloud Scheduler event handlers.
24
+ */
25
+ export declare class CloudSchedulerFixture implements Fixture {
26
+ /**
27
+ * The parent {@link AppFixture}.
28
+ */
29
+ private appFixture;
30
+ init(appFixture: AppFixture): Promise<undefined>;
31
+ /**
32
+ * Creates a {@link CloudSchedulerEventRequester} for an endpoint handling Cloud Scheduler events.
33
+ *
34
+ * @param endpoint The endpoint to query.
35
+ * @param options Options when creating the requester.
36
+ * @returns The {@link CloudSchedulerEventRequester}.
37
+ */
38
+ makeRequester(endpoint: string, options?: CloudSchedulerEventRequesterOptions): CloudSchedulerEventRequester;
39
+ clear(): Promise<void>;
40
+ delete(): Promise<void>;
41
+ }
@@ -0,0 +1,48 @@
1
+ import { HttpStatus } from '@nestjs/common';
2
+ /**
3
+ * A utility class for testing Cloud Scheduler event handlers.
4
+ */
5
+ export class CloudSchedulerFixture {
6
+ /**
7
+ * The parent {@link AppFixture}.
8
+ */
9
+ appFixture;
10
+ async init(appFixture) {
11
+ this.appFixture = appFixture;
12
+ }
13
+ /**
14
+ * Creates a {@link CloudSchedulerEventRequester} for an endpoint handling Cloud Scheduler events.
15
+ *
16
+ * @param endpoint The endpoint to query.
17
+ * @param options Options when creating the requester.
18
+ * @returns The {@link CloudSchedulerEventRequester}.
19
+ */
20
+ makeRequester(endpoint, options = {}) {
21
+ return async (event, requestOptions) => {
22
+ const jobInfo = {
23
+ jobName: 'jobName',
24
+ ...options.jobInfo,
25
+ ...requestOptions?.jobInfo,
26
+ };
27
+ const headers = {
28
+ 'x-cloudscheduler-jobname': jobInfo.jobName,
29
+ };
30
+ if (jobInfo.scheduleTime !== undefined) {
31
+ headers['x-cloudscheduler-scheduletime'] =
32
+ jobInfo.scheduleTime.toISOString();
33
+ }
34
+ const expectedStatus = requestOptions?.expectedStatus ??
35
+ options.expectedStatus ??
36
+ HttpStatus.OK;
37
+ await this.appFixture.request
38
+ .post(endpoint)
39
+ .set(headers)
40
+ .send(event)
41
+ .expect(expectedStatus);
42
+ };
43
+ }
44
+ async clear() { }
45
+ async delete() {
46
+ this.appFixture = undefined;
47
+ }
48
+ }