@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.
- package/README.md +11 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pubsub/interceptor.d.ts +4 -3
- package/dist/pubsub/interceptor.js +6 -5
- package/dist/pubsub/testing.d.ts +45 -7
- package/dist/pubsub/testing.js +56 -21
- package/dist/scheduler/cloud-scheduler-info.d.ts +13 -0
- package/dist/scheduler/cloud-scheduler-info.js +36 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +3 -0
- package/dist/scheduler/interceptor.d.ts +34 -0
- package/dist/scheduler/interceptor.js +98 -0
- package/dist/scheduler/scheduler-event-info.decorator.d.ts +14 -0
- package/dist/scheduler/scheduler-event-info.decorator.js +10 -0
- package/dist/scheduler/testing.d.ts +41 -0
- package/dist/scheduler/testing.js +48 -0
- package/dist/spanner/entity-manager.d.ts +8 -1
- package/dist/spanner/entity-manager.js +9 -2
- package/dist/spanner/error-converter.js +10 -3
- package/dist/spanner/errors.d.ts +1 -1
- package/dist/spanner/errors.js +2 -2
- package/dist/spanner/types.d.ts +6 -0
- package/dist/tasks/cloud-tasks-info.d.ts +35 -0
- package/dist/tasks/cloud-tasks-info.js +90 -0
- package/dist/tasks/index.d.ts +3 -0
- package/dist/tasks/index.js +3 -0
- package/dist/tasks/interceptor.d.ts +30 -0
- package/dist/tasks/interceptor.js +90 -0
- package/dist/tasks/task-event-info.decorator.d.ts +14 -0
- package/dist/tasks/task-event-info.decorator.js +8 -0
- package/dist/tasks/testing.d.ts +41 -0
- package/dist/tasks/testing.js +59 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +6 -0
- 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 `
|
|
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 };
|
package/dist/pubsub/testing.d.ts
CHANGED
|
@@ -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?:
|
|
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
|
package/dist/pubsub/testing.js
CHANGED
|
@@ -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
|
-
*
|
|
147
|
+
* Gets the received messages for the specified topic.
|
|
147
148
|
*
|
|
148
149
|
* @param topic The original name of the event topic.
|
|
149
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
166
|
-
if (
|
|
167
|
-
// This throws with a clearer message
|
|
168
|
-
//
|
|
169
|
-
expect(
|
|
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.
|
|
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 =
|
|
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,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
|
+
}
|