@causa/runtime-google 1.5.3 → 1.6.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.
Files changed (35) 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.js +4 -0
  35. package/package.json +12 -12
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
+ }
@@ -2,7 +2,7 @@ import { Database } from '@google-cloud/spanner';
2
2
  import type { ExecuteSqlRequest, TimestampBounds } from '@google-cloud/spanner/build/src/transaction.js';
3
3
  import { type Type } from '@nestjs/common';
4
4
  import { SpannerTableCache } from './table-cache.js';
5
- import type { SpannerKey, SpannerReadOnlyTransaction, SpannerReadOnlyTransactionOption, SpannerReadWriteTransaction, SpannerReadWriteTransactionOption, SqlParamType, SqlStatement } from './types.js';
5
+ import type { SpannerKey, SpannerReadOnlyTransaction, SpannerReadOnlyTransactionOption, SpannerReadWriteTransaction, SpannerReadWriteTransactionOption, SqlParamFieldType, SqlParamType, SqlStatement } from './types.js';
6
6
  /**
7
7
  * Options for {@link SpannerEntityManager.snapshot}.
8
8
  */
@@ -291,6 +291,13 @@ export declare class SpannerEntityManager {
291
291
  static readonly ParamTypeJsonArray: SqlParamType;
292
292
  static readonly ParamTypeTimestampArray: SqlParamType;
293
293
  static readonly ParamTypeDateArray: SqlParamType;
294
+ static ParamTypeStructArray(...fields: SqlParamFieldType[]): {
295
+ type: string;
296
+ child: {
297
+ type: string;
298
+ fields: SqlParamFieldType[];
299
+ };
300
+ };
294
301
  /**
295
302
  * Converts the given entity or array of entities to Spanner objects, grouping them by table name.
296
303
  *
@@ -12,7 +12,7 @@ import { Database, Snapshot } from '@google-cloud/spanner';
12
12
  import { Injectable } from '@nestjs/common';
13
13
  import { copyInstanceWithMissingColumnsToNull, instanceToSpannerObject, spannerObjectToInstance, updateInstanceByColumn, } from './conversion.js';
14
14
  import { convertSpannerToEntityError } from './error-converter.js';
15
- import { InvalidArgumentError, TransactionFinishedError } from './errors.js';
15
+ import { InvalidArgumentError, TemporarySpannerError, TransactionFinishedError, } from './errors.js';
16
16
  import { SpannerTableCache } from './table-cache.js';
17
17
  /**
18
18
  * A class that manages access to entities stored in a Cloud Spanner database.
@@ -241,7 +241,11 @@ let SpannerEntityManager = class SpannerEntityManager {
241
241
  transaction.end();
242
242
  }
243
243
  }
244
- throw error;
244
+ // If the error was already converted and detected as temporary, the original Spanner error is thrown such
245
+ // that the Spanner client can apply its retry logic. If it decides not to retry, the error will be
246
+ // converted again outside the transaction.
247
+ const isTemporaryWithCause = error instanceof TemporarySpannerError && error.cause;
248
+ throw isTemporaryWithCause ? error.cause : error;
245
249
  }
246
250
  });
247
251
  }
@@ -383,6 +387,9 @@ let SpannerEntityManager = class SpannerEntityManager {
383
387
  type: 'array',
384
388
  child: { type: 'date' },
385
389
  };
390
+ static ParamTypeStructArray(...fields) {
391
+ return { type: 'array', child: { type: 'struct', fields } };
392
+ }
386
393
  /**
387
394
  * Converts the given entity or array of entities to Spanner objects, grouping them by table name.
388
395
  *
@@ -1,7 +1,7 @@
1
- import { EntityAlreadyExistsError } from '@causa/runtime';
1
+ import { EntityAlreadyExistsError, RetryableError } from '@causa/runtime';
2
2
  import { SessionPoolExhaustedError } from '@google-cloud/spanner/build/src/session-pool.js';
3
3
  import { status } from '@grpc/grpc-js';
4
- import { InvalidArgumentError, InvalidQueryError, TemporarySpannerError, } from './errors.js';
4
+ import { InvalidArgumentError, InvalidQueryError, TemporarySpannerError, UnexpectedSpannerError, } from './errors.js';
5
5
  /**
6
6
  * Converts an error thrown by Spanner to an entity error or a Spanner error subclass.
7
7
  *
@@ -9,6 +9,11 @@ import { InvalidArgumentError, InvalidQueryError, TemporarySpannerError, } from
9
9
  * @returns The specific error, or undefined if it could not be converted.
10
10
  */
11
11
  export function convertSpannerToEntityError(error) {
12
+ // Those are errors that have already been converted (or that don't come from Spanner).
13
+ if (error instanceof RetryableError ||
14
+ error instanceof UnexpectedSpannerError) {
15
+ return;
16
+ }
12
17
  // Those are not gRPC errors and are thrown by the session pool.
13
18
  if (error instanceof SessionPoolExhaustedError ||
14
19
  error.message == 'Timeout occurred while acquiring session.') {
@@ -31,7 +36,9 @@ export function convertSpannerToEntityError(error) {
31
36
  case status.UNAVAILABLE:
32
37
  case status.ABORTED:
33
38
  case status.RESOURCE_EXHAUSTED:
34
- return new TemporarySpannerError(error.message, error.code);
39
+ return new TemporarySpannerError(error.message, error.code, {
40
+ cause: error,
41
+ });
35
42
  default:
36
43
  return;
37
44
  }
@@ -45,7 +45,7 @@ export declare class InvalidArgumentError extends UnexpectedSpannerError {
45
45
  */
46
46
  export declare class TemporarySpannerError extends RetryableError {
47
47
  readonly code?: status | undefined;
48
- constructor(message: string, code?: status | undefined);
48
+ constructor(message: string, code?: status | undefined, options?: ErrorOptions);
49
49
  /**
50
50
  * Creates a new {@link TemporarySpannerError} that can be thrown to retry a transaction using the Spanner client
51
51
  * retry mechanism.
@@ -57,8 +57,8 @@ export class InvalidArgumentError extends UnexpectedSpannerError {
57
57
  */
58
58
  export class TemporarySpannerError extends RetryableError {
59
59
  code;
60
- constructor(message, code) {
61
- super(message);
60
+ constructor(message, code, options) {
61
+ super(message, undefined, options);
62
62
  this.code = code;
63
63
  }
64
64
  /**
@@ -1,6 +1,12 @@
1
1
  import { protos, Snapshot, Transaction } from '@google-cloud/spanner';
2
2
  import type { Type } from '@google-cloud/spanner/build/src/codec.js';
3
3
  export type SqlParamType = Type;
4
+ /**
5
+ * The type of a single `STRUCT` field in a Spanner SQL parameter.
6
+ */
7
+ export type SqlParamFieldType = Type & {
8
+ name: string;
9
+ };
4
10
  export declare const SpannerRequestPriority: typeof protos.google.spanner.v1.RequestOptions.Priority;
5
11
  /**
6
12
  * A key for a Spanner row.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Information about a Cloud Tasks task, extracted from HTTP headers.
3
+ */
4
+ export declare class CloudTasksInfo {
5
+ /**
6
+ * The name of the queue.
7
+ */
8
+ readonly queueName: string;
9
+ /**
10
+ * The short name of the task, or a unique system-generated ID if no name was specified at creation.
11
+ */
12
+ readonly taskName: string;
13
+ /**
14
+ * The number of times this task has been retried. For the first attempt, this value is `0`.
15
+ */
16
+ readonly retryCount: number;
17
+ /**
18
+ * The total number of times that the task has received a response from the handler.
19
+ */
20
+ readonly executionCount: number;
21
+ /**
22
+ * The schedule time of the task.
23
+ */
24
+ readonly eta: Date;
25
+ /**
26
+ * The HTTP response code from the previous retry.
27
+ * Only present if this is a retry attempt.
28
+ */
29
+ readonly previousResponse?: number;
30
+ /**
31
+ * The reason for retrying the task.
32
+ * Only present if this is a retry attempt.
33
+ */
34
+ readonly retryReason?: string;
35
+ }
@@ -0,0 +1,90 @@
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 } from '@causa/runtime';
11
+ import { Expose, Transform } from 'class-transformer';
12
+ import { IsDate, IsInt, IsString, Min } from 'class-validator';
13
+ /**
14
+ * Information about a Cloud Tasks task, extracted from HTTP headers.
15
+ */
16
+ export class CloudTasksInfo {
17
+ /**
18
+ * The name of the queue.
19
+ */
20
+ queueName;
21
+ /**
22
+ * The short name of the task, or a unique system-generated ID if no name was specified at creation.
23
+ */
24
+ taskName;
25
+ /**
26
+ * The number of times this task has been retried. For the first attempt, this value is `0`.
27
+ */
28
+ retryCount;
29
+ /**
30
+ * The total number of times that the task has received a response from the handler.
31
+ */
32
+ executionCount;
33
+ /**
34
+ * The schedule time of the task.
35
+ */
36
+ eta;
37
+ /**
38
+ * The HTTP response code from the previous retry.
39
+ * Only present if this is a retry attempt.
40
+ */
41
+ previousResponse;
42
+ /**
43
+ * The reason for retrying the task.
44
+ * Only present if this is a retry attempt.
45
+ */
46
+ retryReason;
47
+ }
48
+ __decorate([
49
+ Expose({ name: 'x-cloudtasks-queuename' }),
50
+ IsString(),
51
+ __metadata("design:type", String)
52
+ ], CloudTasksInfo.prototype, "queueName", void 0);
53
+ __decorate([
54
+ Expose({ name: 'x-cloudtasks-taskname' }),
55
+ IsString(),
56
+ __metadata("design:type", String)
57
+ ], CloudTasksInfo.prototype, "taskName", void 0);
58
+ __decorate([
59
+ Expose({ name: 'x-cloudtasks-taskretrycount' }),
60
+ Transform(({ value }) => parseInt(value)),
61
+ IsInt(),
62
+ Min(0),
63
+ __metadata("design:type", Number)
64
+ ], CloudTasksInfo.prototype, "retryCount", void 0);
65
+ __decorate([
66
+ Expose({ name: 'x-cloudtasks-taskexecutioncount' }),
67
+ Transform(({ value }) => parseInt(value)),
68
+ IsInt(),
69
+ Min(0),
70
+ __metadata("design:type", Number)
71
+ ], CloudTasksInfo.prototype, "executionCount", void 0);
72
+ __decorate([
73
+ Expose({ name: 'x-cloudtasks-tasketa' }),
74
+ Transform(({ value }) => new Date(parseFloat(value) * 1000)),
75
+ IsDate(),
76
+ __metadata("design:type", Date)
77
+ ], CloudTasksInfo.prototype, "eta", void 0);
78
+ __decorate([
79
+ Expose({ name: 'x-cloudtasks-taskpreviousresponse' }),
80
+ Transform(({ value }) => (value !== undefined ? parseInt(value) : undefined)),
81
+ IsInt(),
82
+ AllowMissing(),
83
+ __metadata("design:type", Number)
84
+ ], CloudTasksInfo.prototype, "previousResponse", void 0);
85
+ __decorate([
86
+ Expose({ name: 'x-cloudtasks-taskretryreason' }),
87
+ IsString(),
88
+ AllowMissing(),
89
+ __metadata("design:type", String)
90
+ ], CloudTasksInfo.prototype, "retryReason", void 0);
@@ -1,4 +1,7 @@
1
+ export { CloudTasksInfo } from './cloud-tasks-info.js';
1
2
  export * from './errors.js';
3
+ export { CLOUD_TASKS_EVENT_HANDLER_ID, CloudTasksEventHandlerInterceptor, } from './interceptor.js';
2
4
  export { CloudTasksModule } from './module.js';
3
5
  export { CloudTasksScheduler, HttpMethod } from './scheduler.js';
4
6
  export type { HttpRequest, Task } from './scheduler.js';
7
+ export { CloudTasksEventInfo } from './task-event-info.decorator.js';
@@ -1,3 +1,6 @@
1
+ export { CloudTasksInfo } from './cloud-tasks-info.js';
1
2
  export * from './errors.js';
3
+ export { CLOUD_TASKS_EVENT_HANDLER_ID, CloudTasksEventHandlerInterceptor, } from './interceptor.js';
2
4
  export { CloudTasksModule } from './module.js';
3
5
  export { CloudTasksScheduler, HttpMethod } from './scheduler.js';
6
+ export { CloudTasksEventInfo } from './task-event-info.decorator.js';
@@ -0,0 +1,30 @@
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 { CloudTasksInfo } from './cloud-tasks-info.js';
6
+ /**
7
+ * The ID of the Cloud Tasks event handler interceptor, that can passed to the `UseEventHandler` decorator.
8
+ */
9
+ export declare const CLOUD_TASKS_EVENT_HANDLER_ID = "google.cloudTasks";
10
+ /**
11
+ * The interceptor that should be added to controllers handling Cloud Tasks events.
12
+ */
13
+ export declare class CloudTasksEventHandlerInterceptor extends BaseEventHandlerInterceptor {
14
+ constructor(reflector: Reflector, logger: Logger, options?: EventHandlerInterceptorOptions);
15
+ /**
16
+ * Parses the Cloud Tasks request, extracting task information from headers.
17
+ *
18
+ * @param request The express request object.
19
+ * @returns The parsed Cloud Tasks information.
20
+ */
21
+ protected parseCloudTasksRequest(request: Request): Promise<CloudTasksInfo>;
22
+ protected parseEventFromContext(context: ExecutionContext, dataType: Type): Promise<ParsedEventRequest>;
23
+ /**
24
+ * Creates a Cloud Tasks event handler interceptor class with the given options.
25
+ *
26
+ * @param options Options for the interceptor.
27
+ * @returns The Cloud Tasks event handler interceptor class.
28
+ */
29
+ static withOptions(options: EventHandlerInterceptorOptions): Type<CloudTasksEventHandlerInterceptor>;
30
+ }
@@ -0,0 +1,90 @@
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 CloudTasksEventHandlerInterceptor_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 { CloudTasksInfo } from './cloud-tasks-info.js';
16
+ /**
17
+ * The ID of the Cloud Tasks event handler interceptor, that can passed to the `UseEventHandler` decorator.
18
+ */
19
+ export const CLOUD_TASKS_EVENT_HANDLER_ID = 'google.cloudTasks';
20
+ /**
21
+ * The interceptor that should be added to controllers handling Cloud Tasks events.
22
+ */
23
+ let CloudTasksEventHandlerInterceptor = CloudTasksEventHandlerInterceptor_1 = class CloudTasksEventHandlerInterceptor extends BaseEventHandlerInterceptor {
24
+ constructor(reflector, logger, options = {}) {
25
+ super(CLOUD_TASKS_EVENT_HANDLER_ID, reflector, logger, options);
26
+ this.logger.setContext(CloudTasksEventHandlerInterceptor_1.name);
27
+ }
28
+ /**
29
+ * Parses the Cloud Tasks request, extracting task information from headers.
30
+ *
31
+ * @param request The express request object.
32
+ * @returns The parsed Cloud Tasks information.
33
+ */
34
+ async parseCloudTasksRequest(request) {
35
+ try {
36
+ const info = await parseObject(CloudTasksInfo, request.headers, {
37
+ ...validatorOptions,
38
+ forbidNonWhitelisted: false,
39
+ });
40
+ this.assignEventId(info.taskName);
41
+ this.logger.info('Successfully parsed Cloud Tasks request.');
42
+ return info;
43
+ }
44
+ catch (error) {
45
+ this.logger.error({
46
+ error: error.stack,
47
+ ...(error instanceof ValidationError
48
+ ? { validationMessages: error.validationMessages }
49
+ : {}),
50
+ }, 'Received invalid Cloud Tasks request.');
51
+ throwHttpErrorResponse(new BadRequestErrorDto());
52
+ }
53
+ }
54
+ async parseEventFromContext(context, dataType) {
55
+ const request = context
56
+ .switchToHttp()
57
+ .getRequest();
58
+ request.cloudTasksInfo = await this.parseCloudTasksRequest(request);
59
+ return await this.wrapParsing(async () => {
60
+ const body = await parseObject(dataType, request.body, {
61
+ forbidNonWhitelisted: false,
62
+ });
63
+ return { attributes: {}, body };
64
+ });
65
+ }
66
+ /**
67
+ * Creates a Cloud Tasks event handler interceptor class with the given options.
68
+ *
69
+ * @param options Options for the interceptor.
70
+ * @returns The Cloud Tasks event handler interceptor class.
71
+ */
72
+ static withOptions(options) {
73
+ let CloudTasksEventHandlerInterceptorWithOptions = class CloudTasksEventHandlerInterceptorWithOptions extends CloudTasksEventHandlerInterceptor_1 {
74
+ constructor(reflector, logger) {
75
+ super(reflector, logger, options);
76
+ }
77
+ };
78
+ CloudTasksEventHandlerInterceptorWithOptions = __decorate([
79
+ Injectable(),
80
+ __metadata("design:paramtypes", [Reflector, Logger])
81
+ ], CloudTasksEventHandlerInterceptorWithOptions);
82
+ return CloudTasksEventHandlerInterceptorWithOptions;
83
+ }
84
+ };
85
+ CloudTasksEventHandlerInterceptor = CloudTasksEventHandlerInterceptor_1 = __decorate([
86
+ Injectable(),
87
+ __metadata("design:paramtypes", [Reflector,
88
+ Logger, Object])
89
+ ], CloudTasksEventHandlerInterceptor);
90
+ export { CloudTasksEventHandlerInterceptor };
@@ -0,0 +1,14 @@
1
+ import type { CloudTasksInfo } from './cloud-tasks-info.js';
2
+ /**
3
+ * Additional information expected to be present on an express request that was parsed as a Cloud Tasks request.
4
+ */
5
+ export type RequestWithCloudTasksInfo = {
6
+ /**
7
+ * Information about the Cloud Tasks task.
8
+ */
9
+ cloudTasksInfo: CloudTasksInfo;
10
+ };
11
+ /**
12
+ * Decorates a route handler's parameter to populate it with information about the Cloud Tasks task.
13
+ */
14
+ export declare const CloudTasksEventInfo: (...dataOrPipes: unknown[]) => ParameterDecorator;
@@ -0,0 +1,8 @@
1
+ import { createParamDecorator } from '@nestjs/common';
2
+ /**
3
+ * Decorates a route handler's parameter to populate it with information about the Cloud Tasks task.
4
+ */
5
+ export const CloudTasksEventInfo = createParamDecorator((_data, ctx) => {
6
+ const request = ctx.switchToHttp().getRequest();
7
+ return request.cloudTasksInfo;
8
+ });
@@ -0,0 +1,41 @@
1
+ import type { AppFixture, Fixture } from '@causa/runtime/nestjs/testing';
2
+ import type { CloudTasksInfo } from './cloud-tasks-info.js';
3
+ /**
4
+ * Options when making a request to an endpoint handling Cloud Tasks events using a {@link CloudTasksEventRequester}.
5
+ */
6
+ export type CloudTasksEventRequesterOptions = {
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 Tasks task to include in the request headers.
14
+ * If not provided, default values will be used.
15
+ */
16
+ readonly taskInfo?: Partial<CloudTasksInfo>;
17
+ };
18
+ /**
19
+ * A function that makes a query to an endpoint handling Cloud Tasks events and tests the response.
20
+ */
21
+ export type CloudTasksEventRequester = (event: object, options?: CloudTasksEventRequesterOptions) => Promise<void>;
22
+ /**
23
+ * A utility class for testing Cloud Tasks event handlers.
24
+ */
25
+ export declare class CloudTasksFixture implements Fixture {
26
+ /**
27
+ * The parent {@link AppFixture}.
28
+ */
29
+ private appFixture;
30
+ init(appFixture: AppFixture): Promise<undefined>;
31
+ /**
32
+ * Creates a {@link CloudTasksEventRequester} for an endpoint handling Cloud Tasks events.
33
+ *
34
+ * @param endpoint The endpoint to query.
35
+ * @param options Options when creating the requester.
36
+ * @returns The {@link CloudTasksEventRequester}.
37
+ */
38
+ makeRequester(endpoint: string, options?: CloudTasksEventRequesterOptions): CloudTasksEventRequester;
39
+ clear(): Promise<void>;
40
+ delete(): Promise<void>;
41
+ }
@@ -0,0 +1,59 @@
1
+ import { HttpStatus } from '@nestjs/common';
2
+ import { randomUUID } from 'node:crypto';
3
+ /**
4
+ * A utility class for testing Cloud Tasks event handlers.
5
+ */
6
+ export class CloudTasksFixture {
7
+ /**
8
+ * The parent {@link AppFixture}.
9
+ */
10
+ appFixture;
11
+ async init(appFixture) {
12
+ this.appFixture = appFixture;
13
+ }
14
+ /**
15
+ * Creates a {@link CloudTasksEventRequester} for an endpoint handling Cloud Tasks events.
16
+ *
17
+ * @param endpoint The endpoint to query.
18
+ * @param options Options when creating the requester.
19
+ * @returns The {@link CloudTasksEventRequester}.
20
+ */
21
+ makeRequester(endpoint, options = {}) {
22
+ return async (event, requestOptions) => {
23
+ const taskInfo = {
24
+ queueName: 'queueName',
25
+ taskName: randomUUID(),
26
+ retryCount: 0,
27
+ executionCount: 0,
28
+ eta: new Date(),
29
+ ...options.taskInfo,
30
+ ...requestOptions?.taskInfo,
31
+ };
32
+ const headers = {
33
+ 'x-cloudtasks-queuename': taskInfo.queueName,
34
+ 'x-cloudtasks-taskname': taskInfo.taskName,
35
+ 'x-cloudtasks-taskretrycount': String(taskInfo.retryCount),
36
+ 'x-cloudtasks-taskexecutioncount': String(taskInfo.executionCount),
37
+ 'x-cloudtasks-tasketa': (taskInfo.eta.getTime() / 1000).toFixed(3),
38
+ };
39
+ if (taskInfo.previousResponse !== undefined) {
40
+ headers['x-cloudtasks-taskpreviousresponse'] = String(taskInfo.previousResponse);
41
+ }
42
+ if (taskInfo.retryReason !== undefined) {
43
+ headers['x-cloudtasks-taskretryreason'] = taskInfo.retryReason;
44
+ }
45
+ const expectedStatus = requestOptions?.expectedStatus ??
46
+ options.expectedStatus ??
47
+ HttpStatus.OK;
48
+ await this.appFixture.request
49
+ .post(endpoint)
50
+ .set(headers)
51
+ .send(event)
52
+ .expect(expectedStatus);
53
+ };
54
+ }
55
+ async clear() { }
56
+ async delete() {
57
+ this.appFixture = undefined;
58
+ }
59
+ }
package/dist/testing.js CHANGED
@@ -3,7 +3,9 @@ import { VersionedEntityFixture } from '@causa/runtime/testing';
3
3
  import { FirestoreFixture } from './firestore/testing.js';
4
4
  import { AuthUsersFixture } from './identity-platform/testing.js';
5
5
  import { PubSubFixture } from './pubsub/testing.js';
6
+ import { CloudSchedulerFixture } from './scheduler/testing.js';
6
7
  import { SpannerFixture } from './spanner/testing.js';
8
+ import { CloudTasksFixture } from './tasks/testing.js';
7
9
  import { AppCheckFixture, FirebaseFixture } from './testing.js';
8
10
  import { FirestorePubSubTransactionRunner, SpannerOutboxTransactionRunner, } from './transaction/index.js';
9
11
  export * from './app-check/testing.js';
@@ -34,5 +36,7 @@ export function createGoogleFixtures(options = {}) {
34
36
  new PubSubFixture(options.pubSubTopics ?? {}),
35
37
  ...(disableAppCheck ? [new AppCheckFixture()] : []),
36
38
  ...versionedEntityFixture,
39
+ new CloudTasksFixture(),
40
+ new CloudSchedulerFixture(),
37
41
  ];
38
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causa/runtime-google",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "An extension to the Causa runtime SDK (`@causa/runtime`), providing Google-specific features.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,15 +32,15 @@
32
32
  "test:cov": "npm run test -- --coverage"
33
33
  },
34
34
  "dependencies": {
35
- "@causa/runtime": "^1.5.0",
35
+ "@causa/runtime": "^1.6.0",
36
36
  "@google-cloud/precise-date": "^5.0.0",
37
- "@google-cloud/pubsub": "^5.2.1",
38
- "@google-cloud/spanner": "^8.4.0",
37
+ "@google-cloud/pubsub": "^5.2.2",
38
+ "@google-cloud/spanner": "^8.5.0",
39
39
  "@google-cloud/tasks": "^6.2.1",
40
40
  "@grpc/grpc-js": "^1.14.3",
41
- "@nestjs/common": "^11.1.11",
41
+ "@nestjs/common": "^11.1.12",
42
42
  "@nestjs/config": "^4.0.2",
43
- "@nestjs/core": "^11.1.11",
43
+ "@nestjs/core": "^11.1.12",
44
44
  "@nestjs/passport": "^11.0.5",
45
45
  "@nestjs/terminus": "^11.0.0",
46
46
  "class-transformer": "^0.5.1",
@@ -49,31 +49,31 @@
49
49
  "firebase-admin": "^13.6.0",
50
50
  "jsonwebtoken": "^9.0.3",
51
51
  "passport-http-bearer": "^1.0.1",
52
- "pino": "^10.1.1",
52
+ "pino": "^10.3.0",
53
53
  "reflect-metadata": "^0.2.2"
54
54
  },
55
55
  "devDependencies": {
56
- "@nestjs/testing": "^11.1.11",
57
- "@swc/core": "^1.15.8",
56
+ "@nestjs/testing": "^11.1.12",
57
+ "@swc/core": "^1.15.10",
58
58
  "@swc/jest": "^0.2.39",
59
59
  "@tsconfig/node20": "^20.1.8",
60
60
  "@types/jest": "^30.0.0",
61
61
  "@types/jsonwebtoken": "^9.0.10",
62
- "@types/node": "^20.19.28",
62
+ "@types/node": "^20.19.30",
63
63
  "@types/passport-http-bearer": "^1.0.42",
64
64
  "@types/supertest": "^6.0.3",
65
65
  "@types/uuid": "^11.0.0",
66
66
  "dotenv": "^17.2.3",
67
67
  "eslint": "^9.39.2",
68
68
  "eslint-config-prettier": "^10.1.8",
69
- "eslint-plugin-prettier": "^5.5.4",
69
+ "eslint-plugin-prettier": "^5.5.5",
70
70
  "jest": "^30.2.0",
71
71
  "jest-extended": "^7.0.0",
72
72
  "pino-pretty": "^13.1.3",
73
73
  "rimraf": "^6.1.2",
74
74
  "supertest": "^7.2.2",
75
75
  "typescript": "^5.9.3",
76
- "typescript-eslint": "^8.52.0",
76
+ "typescript-eslint": "^8.53.1",
77
77
  "uuid": "^13.0.0"
78
78
  }
79
79
  }