@causa/workspace-google 0.15.0 → 0.17.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.
- package/README.md +12 -1
- package/dist/configurations/generated.d.ts +6 -0
- package/dist/configurations/generated.js +11 -0
- package/dist/configurations/schemas/google.yaml +10 -0
- package/dist/functions/event-topic/broker-create-trigger-cloud-run.call.d.ts +1 -1
- package/dist/functions/event-topic/broker-create-trigger-cloud-run.call.js +41 -11
- package/dist/functions/event-topic/broker-create-trigger-cloud-run.d.ts +11 -2
- package/dist/functions/event-topic/broker-create-trigger-cloud-run.js +18 -6
- package/dist/functions/event-topic/broker-publish-events-google.call.js +42 -26
- package/dist/functions/event-topic/create-backfill-source-bigquery.call.d.ts +4 -0
- package/dist/functions/event-topic/create-backfill-source-bigquery.call.js +48 -0
- package/dist/functions/event-topic/create-backfill-source-bigquery.d.ts +20 -0
- package/dist/functions/event-topic/create-backfill-source-bigquery.js +32 -0
- package/dist/functions/event-topic/index.d.ts +2 -0
- package/dist/functions/event-topic/index.js +2 -0
- package/dist/functions/event-topic/query-events-bigquery.call.d.ts +4 -0
- package/dist/functions/event-topic/query-events-bigquery.call.js +41 -0
- package/dist/functions/event-topic/query-events-bigquery.d.ts +10 -0
- package/dist/functions/event-topic/query-events-bigquery.js +17 -0
- package/dist/functions/google-firestore/index.d.ts +1 -0
- package/dist/functions/google-firestore/index.js +1 -0
- package/dist/functions/google-firestore/query-records.call.d.ts +3 -0
- package/dist/functions/google-firestore/query-records.call.js +50 -0
- package/dist/functions/google-firestore/query-records.d.ts +18 -0
- package/dist/functions/google-firestore/query-records.js +22 -0
- package/dist/functions/google-spanner/index.d.ts +1 -0
- package/dist/functions/google-spanner/index.js +1 -0
- package/dist/functions/google-spanner/query-records.call.d.ts +3 -0
- package/dist/functions/google-spanner/query-records.call.js +66 -0
- package/dist/functions/google-spanner/query-records.d.ts +23 -0
- package/dist/functions/google-spanner/query-records.js +27 -0
- package/dist/functions/index.js +5 -4
- package/dist/functions/service-container/index.d.ts +1 -0
- package/dist/functions/service-container/index.js +1 -0
- package/dist/functions/service-container/query-logs-cloud-run.call.d.ts +4 -0
- package/dist/functions/service-container/query-logs-cloud-run.call.js +36 -0
- package/dist/functions/service-container/query-logs-cloud-run.d.ts +12 -0
- package/dist/functions/service-container/query-logs-cloud-run.js +18 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/logging.d.ts +16 -0
- package/dist/services/logging.js +20 -0
- package/package.json +18 -17
- package/dist/backfilling/bigquery.d.ts +0 -53
- package/dist/backfilling/bigquery.js +0 -101
- package/dist/backfilling/index.d.ts +0 -2
- package/dist/backfilling/index.js +0 -2
- package/dist/backfilling/pubsub.d.ts +0 -34
- package/dist/backfilling/pubsub.js +0 -65
package/README.md
CHANGED
|
@@ -55,10 +55,21 @@ The following emulators are implemented:
|
|
|
55
55
|
|
|
56
56
|
### Backfilling
|
|
57
57
|
|
|
58
|
-
Backfilling is supported when `google.pubSub` is set as the `events.broker`. Temporary triggers can be created for Cloud Run services
|
|
58
|
+
Backfilling is supported when `google.pubSub` is set as the `events.broker`. Temporary triggers can be created for Cloud Run services in two ways:
|
|
59
|
+
|
|
60
|
+
- As a raw URI, using the format `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
|
|
61
|
+
- As a project-scoped trigger, using the format `<projectPath>#<triggerName>[?<options>]`. The referenced project must be of type `serviceContainer` with `google.cloudRun` as `serviceContainer.platform`. The HTTP path is read from `serviceContainer.triggers[<triggerName>].endpoint` (whose `type` must be `http`), and the `options` query string is forwarded as URL query parameters. The target Cloud Run service name defaults to `project.name`, and can be overridden by setting `google.cloudRun.eventBackfillServiceName`.
|
|
59
62
|
|
|
60
63
|
When no source is specified, the default is to fetch events to backfill from the BigQuery dataset configured in `google.pubSub.bigQueryStorage`. A custom BigQuery table can also be set as source using the `bq://<projectId>.<datasetId>.<tableId>` format. It should have the `data` and `attributes` columns.
|
|
61
64
|
|
|
65
|
+
### Querying
|
|
66
|
+
|
|
67
|
+
This module implements the `DatabaseQueryRecords`, `ServiceContainerQueryLogs`, and `EventTopicQueryEvents` workspace functions, which expose read-only access to data stored in or emitted by deployed services:
|
|
68
|
+
|
|
69
|
+
- `DatabaseQueryRecords` is supported for the `google.spanner` and `google.firestore` engines. For Spanner, the function takes a database name and a SQL query, runs it on the configured Spanner instance, and returns the matching rows (capped to 100000). For Firestore, the query is the path to a single document, fetched from the default database (or the specified one). The document data is returned as a single-element array. An empty array is returned when the document does not exist.
|
|
70
|
+
- `ServiceContainerQueryLogs` is supported when `serviceContainer.platform` is `google.cloudRun`. It queries Cloud Logging for log entries emitted by the service's Cloud Run revisions. By default, only the last hour of logs and up to 1 000 entries are returned.
|
|
71
|
+
- `EventTopicQueryEvents` is supported when `events.broker` is `google.pubSub`, `events.format` is `json`, and `google.pubSub.bigQueryStorage.rawEventsDatasetId` is set. It reads events from the topic's BigQuery raw events table. Same defaults as for log queries: last hour and up to 1 000 events.
|
|
72
|
+
|
|
62
73
|
### Secrets backend
|
|
63
74
|
|
|
64
75
|
This module implements the `google.secretManager` secret backend, allowing fetching secrets from the Google Secret Manager service. Here are some example of how secrets with the `google.secretManager` backend should be defined:
|
|
@@ -67,6 +67,12 @@ export declare class GoogleCloudRun {
|
|
|
67
67
|
* The Docker repository where Cloud Run containers should be uploaded.
|
|
68
68
|
*/
|
|
69
69
|
readonly dockerRepository?: string;
|
|
70
|
+
/**
|
|
71
|
+
* For `serviceContainer` projects deployed on Cloud Run, the name of the Cloud Run service that receives
|
|
72
|
+
* events during a backfill targeting a project-scoped trigger. Defaults to the project's name
|
|
73
|
+
* (`project.name`) when unset.
|
|
74
|
+
*/
|
|
75
|
+
readonly eventBackfillServiceName?: string;
|
|
70
76
|
/**
|
|
71
77
|
* The type of allowed ingress that can reach the container.
|
|
72
78
|
* See https://cloud.google.com/run/docs/securing/ingress#settings for more details.
|
|
@@ -110,6 +110,12 @@ export class GoogleCloudRun {
|
|
|
110
110
|
* The Docker repository where Cloud Run containers should be uploaded.
|
|
111
111
|
*/
|
|
112
112
|
dockerRepository;
|
|
113
|
+
/**
|
|
114
|
+
* For `serviceContainer` projects deployed on Cloud Run, the name of the Cloud Run service that receives
|
|
115
|
+
* events during a backfill targeting a project-scoped trigger. Defaults to the project's name
|
|
116
|
+
* (`project.name`) when unset.
|
|
117
|
+
*/
|
|
118
|
+
eventBackfillServiceName;
|
|
113
119
|
/**
|
|
114
120
|
* The type of allowed ingress that can reach the container.
|
|
115
121
|
* See https://cloud.google.com/run/docs/securing/ingress#settings for more details.
|
|
@@ -156,6 +162,11 @@ __decorate([
|
|
|
156
162
|
IsString(),
|
|
157
163
|
__metadata("design:type", String)
|
|
158
164
|
], GoogleCloudRun.prototype, "dockerRepository", void 0);
|
|
165
|
+
__decorate([
|
|
166
|
+
AllowMissing(),
|
|
167
|
+
IsString(),
|
|
168
|
+
__metadata("design:type", String)
|
|
169
|
+
], GoogleCloudRun.prototype, "eventBackfillServiceName", void 0);
|
|
159
170
|
__decorate([
|
|
160
171
|
AllowMissing(),
|
|
161
172
|
IsIn(['all', 'internal', 'internal-and-cloud-load-balancing']),
|
|
@@ -358,6 +358,16 @@ properties:
|
|
|
358
358
|
location:
|
|
359
359
|
type: string
|
|
360
360
|
description: The location where Cloud Run services should be deployed.
|
|
361
|
+
eventBackfillServiceName:
|
|
362
|
+
type: string
|
|
363
|
+
description: >-
|
|
364
|
+
For `serviceContainer` projects deployed on Cloud Run, the name of
|
|
365
|
+
the Cloud Run service that receives
|
|
366
|
+
|
|
367
|
+
events during a backfill targeting a project-scoped trigger.
|
|
368
|
+
Defaults to the project's name
|
|
369
|
+
|
|
370
|
+
(`project.name`) when unset.
|
|
361
371
|
secretEnvironmentVariables:
|
|
362
372
|
type: object
|
|
363
373
|
description: >-
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { WorkspaceContext } from '@causa/workspace';
|
|
2
|
-
import type
|
|
2
|
+
import { type EventTopicBrokerCreateTriggerForCloudRun } from './broker-create-trigger-cloud-run.js';
|
|
3
3
|
export default function call(this: EventTopicBrokerCreateTriggerForCloudRun, context: WorkspaceContext): Promise<string[]>;
|
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
import { CloudRunPubSubTriggerService } from '../../services/cloud-run-pubsub-trigger.js';
|
|
2
|
+
import { CLOUD_RUN_TRIGGER_ID_REGEX, } from './broker-create-trigger-cloud-run.js';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
* [[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger
|
|
4
|
+
* Resolves the Cloud Run service ID and HTTP path for a raw URI trigger string.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const match = this.trigger.match(CLOUD_RUN_TRIGGER_ID_REGEX);
|
|
6
|
+
function resolveFromString(context, trigger) {
|
|
7
|
+
const match = trigger.match(CLOUD_RUN_TRIGGER_ID_REGEX);
|
|
9
8
|
if (!match?.groups) {
|
|
10
|
-
throw new Error('
|
|
9
|
+
throw new Error(`Invalid Cloud Run trigger '${trigger}'.`);
|
|
11
10
|
}
|
|
12
11
|
const googleConf = context.asConfiguration();
|
|
13
12
|
const projectId = match.groups.projectId ?? googleConf.getOrThrow('google.project');
|
|
14
13
|
const location = match.groups.location ?? googleConf.getOrThrow('google.cloudRun.location');
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
return {
|
|
15
|
+
serviceId: `projects/${projectId}/locations/${location}/services/${match.groups.name}`,
|
|
16
|
+
path: match.groups.path,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the Cloud Run service ID and HTTP path for a project-scoped trigger object.
|
|
21
|
+
* The project is expected to be a `serviceContainer` deployed on Cloud Run. The service name defaults to the project's
|
|
22
|
+
* name, and is overridden by `google.cloudRun.eventBackfillServiceName` when set. The trigger path is read from
|
|
23
|
+
* `serviceContainer.triggers[name].endpoint`, which must be of type `http`. Options are serialized as a URL query
|
|
24
|
+
* string.
|
|
25
|
+
*
|
|
26
|
+
* @param context The {@link WorkspaceContext}.
|
|
27
|
+
* @param trigger The trigger object.
|
|
28
|
+
* @returns The resolved service ID and path.
|
|
29
|
+
*/
|
|
30
|
+
function resolveFromObject(context, trigger) {
|
|
31
|
+
const conf = context.asConfiguration();
|
|
32
|
+
const projectId = conf.getOrThrow('google.project');
|
|
33
|
+
const location = conf.getOrThrow('google.cloudRun.location');
|
|
34
|
+
const serviceName = conf.get('google.cloudRun.eventBackfillServiceName') ??
|
|
35
|
+
conf.getOrThrow('project.name');
|
|
36
|
+
const endpoint = conf.get(`serviceContainer.triggers.${trigger.name}.endpoint`);
|
|
37
|
+
if (!endpoint || endpoint.type !== 'http' || !endpoint.path) {
|
|
38
|
+
throw new Error(`Trigger '${trigger.name}' does not exist or does not define an HTTP endpoint.`);
|
|
19
39
|
}
|
|
20
|
-
const
|
|
40
|
+
const query = new URLSearchParams(trigger.options).toString();
|
|
41
|
+
const path = query ? `${endpoint.path}?${query}` : endpoint.path;
|
|
42
|
+
return {
|
|
43
|
+
serviceId: `projects/${projectId}/locations/${location}/services/${serviceName}`,
|
|
44
|
+
path,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export default async function call(context) {
|
|
48
|
+
const { serviceId, path } = typeof this.trigger === 'string'
|
|
49
|
+
? resolveFromString(context, this.trigger)
|
|
50
|
+
: resolveFromObject(context, this.trigger);
|
|
21
51
|
return await context
|
|
22
52
|
.service(CloudRunPubSubTriggerService)
|
|
23
53
|
.create(this.backfillId, this.topicId, serviceId, path);
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { WorkspaceContext } from '@causa/workspace';
|
|
2
2
|
import { EventTopicBrokerCreateTrigger } from '@causa/workspace-core';
|
|
3
|
+
/**
|
|
4
|
+
* The regular expression used to match Cloud Run trigger IDs, consisting of the service ID and the path to the trigger.
|
|
5
|
+
* [[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger
|
|
6
|
+
*/
|
|
7
|
+
export declare const CLOUD_RUN_TRIGGER_ID_REGEX: RegExp;
|
|
3
8
|
/**
|
|
4
9
|
* Implements {@link EventTopicBrokerCreateTrigger} for Cloud Run endpoints being triggered by Pub/Sub messages.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Two forms of trigger are supported:
|
|
12
|
+
* - A string in the format `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
|
|
13
|
+
* - An object `{ name, options }` passed when the function is called on a project-scoped context. In that case the
|
|
14
|
+
* project must be a `serviceContainer` deployed on Cloud Run. The trigger's `name` must match an entry in
|
|
15
|
+
* `serviceContainer.triggers` whose endpoint is of type `http`. Options are forwarded as URL query parameters.
|
|
7
16
|
*/
|
|
8
17
|
export declare class EventTopicBrokerCreateTriggerForCloudRun extends EventTopicBrokerCreateTrigger {
|
|
9
18
|
_call(context: WorkspaceContext): Promise<string[]>;
|
|
@@ -4,19 +4,31 @@ import { EventTopicBrokerCreateTrigger, } from '@causa/workspace-core';
|
|
|
4
4
|
* The regular expression used to match Cloud Run trigger IDs, consisting of the service ID and the path to the trigger.
|
|
5
5
|
* [[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger
|
|
6
6
|
*/
|
|
7
|
-
const CLOUD_RUN_TRIGGER_ID_REGEX = /^(?:(?:projects\/(?<projectId>[\w-]+)\/)?locations\/(?<location>[\w-]+)\/)?services\/(?<name>[\w-]+)(?<path>\/.*)$/;
|
|
7
|
+
export const CLOUD_RUN_TRIGGER_ID_REGEX = /^(?:(?:projects\/(?<projectId>[\w-]+)\/)?locations\/(?<location>[\w-]+)\/)?services\/(?<name>[\w-]+)(?<path>\/.*)$/;
|
|
8
8
|
/**
|
|
9
9
|
* Implements {@link EventTopicBrokerCreateTrigger} for Cloud Run endpoints being triggered by Pub/Sub messages.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Two forms of trigger are supported:
|
|
12
|
+
* - A string in the format `[[projects/<projectId>/]locations/<location>/]services/<name>/path-to-trigger`.
|
|
13
|
+
* - An object `{ name, options }` passed when the function is called on a project-scoped context. In that case the
|
|
14
|
+
* project must be a `serviceContainer` deployed on Cloud Run. The trigger's `name` must match an entry in
|
|
15
|
+
* `serviceContainer.triggers` whose endpoint is of type `http`. Options are forwarded as URL query parameters.
|
|
12
16
|
*/
|
|
13
17
|
export class EventTopicBrokerCreateTriggerForCloudRun extends EventTopicBrokerCreateTrigger {
|
|
14
18
|
async _call(context) {
|
|
15
19
|
return await callDeferred(this, context, import.meta.url);
|
|
16
20
|
}
|
|
17
21
|
_supports(context) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
const isPubSub = context.asConfiguration().get('events.broker') ===
|
|
23
|
+
'google.pubSub';
|
|
24
|
+
if (!isPubSub) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (typeof this.trigger === 'string') {
|
|
28
|
+
return CLOUD_RUN_TRIGGER_ID_REGEX.test(this.trigger);
|
|
29
|
+
}
|
|
30
|
+
const conf = context.asConfiguration();
|
|
31
|
+
return (conf.get('project.type') === 'serviceContainer' &&
|
|
32
|
+
conf.get('serviceContainer.platform') === 'google.cloudRun');
|
|
21
33
|
}
|
|
22
34
|
}
|
|
@@ -1,29 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { FlowControlledPublisher } from '@google-cloud/pubsub/build/src/publisher/flow-publisher.js';
|
|
2
|
+
import { PubSubService } from '../../services/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The number of events between each progress log line.
|
|
5
|
+
*/
|
|
6
|
+
const PUBLISH_PROGRESS_LOG_INTERVAL = 10000;
|
|
7
|
+
/**
|
|
8
|
+
* Options when publishing events in batches to Pub/Sub.
|
|
9
|
+
*/
|
|
10
|
+
const PUBLISH_OPTIONS = {
|
|
11
|
+
flowControlOptions: {
|
|
12
|
+
maxOutstandingBytes: 10 * 1024 * 1024,
|
|
13
|
+
maxOutstandingMessages: 1000,
|
|
14
|
+
},
|
|
15
|
+
batching: {
|
|
16
|
+
maxBytes: 10 * 1024 * 1024,
|
|
17
|
+
maxMessages: 1000,
|
|
18
|
+
maxMilliseconds: 1000,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export default async function call(context) {
|
|
22
|
+
const publisher = context
|
|
23
|
+
.service(PubSubService)
|
|
24
|
+
.pubSub.topic(this.topicId, PUBLISH_OPTIONS)
|
|
25
|
+
.flowControlled();
|
|
26
|
+
context.logger.info('📫 Publishing events.');
|
|
27
|
+
let numEvents = 0;
|
|
28
|
+
for await (const event of this.source()) {
|
|
29
|
+
numEvents += 1;
|
|
30
|
+
const wait = publisher.publish({
|
|
31
|
+
data: event.data,
|
|
32
|
+
attributes: event.attributes,
|
|
33
|
+
orderingKey: event.key,
|
|
34
|
+
});
|
|
35
|
+
if (wait) {
|
|
36
|
+
context.logger.debug('Waiting for the publisher to catch up before resuming publishing.');
|
|
37
|
+
await wait;
|
|
38
|
+
}
|
|
39
|
+
if (numEvents % PUBLISH_PROGRESS_LOG_INTERVAL === 0) {
|
|
40
|
+
context.logger.debug(`Published ${numEvents} events.`);
|
|
10
41
|
}
|
|
11
|
-
const tableName = self.eventTopic.replace(/[-\.]/g, '_');
|
|
12
|
-
const tableId = `${projectId}.${rawEventsDatasetId}.${tableName}`;
|
|
13
|
-
return new BigQueryEventsSource(context, tableId);
|
|
14
|
-
}
|
|
15
|
-
const bqSource = await BigQueryEventsSource.fromSourceAndFilter(context, self.source, self.filter);
|
|
16
|
-
if (bqSource) {
|
|
17
|
-
return bqSource;
|
|
18
|
-
}
|
|
19
|
-
const jsonFilesSource = await JsonFilesEventSource.fromSourceAndFilter(context, self.source, self.filter);
|
|
20
|
-
if (jsonFilesSource) {
|
|
21
|
-
return jsonFilesSource;
|
|
22
42
|
}
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
export default async function call(context) {
|
|
26
|
-
const source = await createSource(this, context);
|
|
27
|
-
const publisher = new PubSubBackfillEventPublisher(context, this.topicId);
|
|
28
|
-
await publisher.publishFromSource(source);
|
|
43
|
+
await publisher.all();
|
|
44
|
+
context.logger.info(`📫 Finished publishing ${numEvents} events.`);
|
|
29
45
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WorkspaceContext } from '@causa/workspace';
|
|
2
|
+
import type { BackfillEvent } from '@causa/workspace-core';
|
|
3
|
+
import { type EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
|
|
4
|
+
export default function call(this: EventTopicCreateBackfillSourceForBigQuery, context: WorkspaceContext): Promise<AsyncIterable<BackfillEvent>>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BigQueryService } from '../../services/index.js';
|
|
2
|
+
import { BIGQUERY_SOURCE_REGEX, } from './create-backfill-source-bigquery.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the full BigQuery table ID from the function arguments and configuration.
|
|
5
|
+
*
|
|
6
|
+
* @param self The function instance.
|
|
7
|
+
* @param context The {@link WorkspaceContext}.
|
|
8
|
+
* @returns The full BigQuery table ID, in the format `projectId.datasetId.tableId`.
|
|
9
|
+
*/
|
|
10
|
+
function resolveTableId(self, context) {
|
|
11
|
+
if (self.source) {
|
|
12
|
+
const match = self.source.match(BIGQUERY_SOURCE_REGEX);
|
|
13
|
+
if (!match?.groups?.tableId) {
|
|
14
|
+
throw new Error(`The source '${self.source}' is not a valid BigQuery source.`);
|
|
15
|
+
}
|
|
16
|
+
return match.groups.tableId;
|
|
17
|
+
}
|
|
18
|
+
const googleConf = context.asConfiguration();
|
|
19
|
+
const projectId = googleConf.getOrThrow('google.project');
|
|
20
|
+
const rawEventsDatasetId = googleConf.getOrThrow('google.pubSub.bigQueryStorage.rawEventsDatasetId');
|
|
21
|
+
const tableName = self.eventTopic.replace(/[-\.]/g, '_');
|
|
22
|
+
return `${projectId}.${rawEventsDatasetId}.${tableName}`;
|
|
23
|
+
}
|
|
24
|
+
export default async function call(context) {
|
|
25
|
+
const tableId = resolveTableId(this, context);
|
|
26
|
+
let query = `
|
|
27
|
+
SELECT
|
|
28
|
+
data,
|
|
29
|
+
attributes,
|
|
30
|
+
FROM
|
|
31
|
+
\`${tableId}\``;
|
|
32
|
+
if (this.filter) {
|
|
33
|
+
query += `
|
|
34
|
+
WHERE
|
|
35
|
+
(${this.filter})`;
|
|
36
|
+
}
|
|
37
|
+
context.logger.debug(`Creating BigQuery job from query '${query}'.`);
|
|
38
|
+
const bigQuery = context.service(BigQueryService).bigQuery;
|
|
39
|
+
const [job] = await bigQuery.createQueryJob(query);
|
|
40
|
+
return (async function* () {
|
|
41
|
+
for await (const row of job.getQueryResultsStream()) {
|
|
42
|
+
yield {
|
|
43
|
+
data: Buffer.from(row.data),
|
|
44
|
+
attributes: JSON.parse(row.attributes),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { WorkspaceContext } from '@causa/workspace';
|
|
2
|
+
import { type BackfillEvent, EventTopicCreateBackfillSource } from '@causa/workspace-core';
|
|
3
|
+
/**
|
|
4
|
+
* The regular expression used to parse a `bq://<tableId>` source string.
|
|
5
|
+
*/
|
|
6
|
+
export declare const BIGQUERY_SOURCE_REGEX: RegExp;
|
|
7
|
+
/**
|
|
8
|
+
* Implements {@link EventTopicCreateBackfillSource} for BigQuery tables.
|
|
9
|
+
*
|
|
10
|
+
* Two cases are supported:
|
|
11
|
+
* - The `source` is a `bq://<projectId>.<datasetId>.<tableId>` URI.
|
|
12
|
+
* - The `source` is omitted and the broker is `google.pubSub` with `google.pubSub.bigQueryStorage.rawEventsDatasetId`
|
|
13
|
+
* configured: events are read from the default BigQuery storage for the topic.
|
|
14
|
+
*
|
|
15
|
+
* The returned async iterable pages through the query results and yields each row as a {@link BackfillEvent}.
|
|
16
|
+
*/
|
|
17
|
+
export declare class EventTopicCreateBackfillSourceForBigQuery extends EventTopicCreateBackfillSource {
|
|
18
|
+
_call(context: WorkspaceContext): Promise<AsyncIterable<BackfillEvent>>;
|
|
19
|
+
_supports(context: WorkspaceContext): boolean;
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { callDeferred, WorkspaceContext } from '@causa/workspace';
|
|
2
|
+
import { EventTopicCreateBackfillSource, } from '@causa/workspace-core';
|
|
3
|
+
/**
|
|
4
|
+
* The regular expression used to parse a `bq://<tableId>` source string.
|
|
5
|
+
*/
|
|
6
|
+
export const BIGQUERY_SOURCE_REGEX = /^bq:\/\/(?<tableId>.+)$/;
|
|
7
|
+
/**
|
|
8
|
+
* Implements {@link EventTopicCreateBackfillSource} for BigQuery tables.
|
|
9
|
+
*
|
|
10
|
+
* Two cases are supported:
|
|
11
|
+
* - The `source` is a `bq://<projectId>.<datasetId>.<tableId>` URI.
|
|
12
|
+
* - The `source` is omitted and the broker is `google.pubSub` with `google.pubSub.bigQueryStorage.rawEventsDatasetId`
|
|
13
|
+
* configured: events are read from the default BigQuery storage for the topic.
|
|
14
|
+
*
|
|
15
|
+
* The returned async iterable pages through the query results and yields each row as a {@link BackfillEvent}.
|
|
16
|
+
*/
|
|
17
|
+
export class EventTopicCreateBackfillSourceForBigQuery extends EventTopicCreateBackfillSource {
|
|
18
|
+
async _call(context) {
|
|
19
|
+
return await callDeferred(this, context, import.meta.url);
|
|
20
|
+
}
|
|
21
|
+
_supports(context) {
|
|
22
|
+
if (this.source?.match(BIGQUERY_SOURCE_REGEX)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (this.source) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const conf = context.asConfiguration();
|
|
29
|
+
return (conf.get('events.broker') === 'google.pubSub' &&
|
|
30
|
+
!!conf.get('google.pubSub.bigQueryStorage.rawEventsDatasetId'));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -6,3 +6,5 @@ export { EventTopicBrokerDeleteTriggerResourceForPubSubSubscription } from './br
|
|
|
6
6
|
export { EventTopicBrokerDeleteTriggerResourceForServiceAccount } from './broker-delete-trigger-resource-service-account.js';
|
|
7
7
|
export { EventTopicBrokerGetTopicIdForPubSub } from './broker-get-topic-id-pubsub.js';
|
|
8
8
|
export { EventTopicBrokerPublishEventsForGoogle } from './broker-publish-events-google.js';
|
|
9
|
+
export { EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
|
|
10
|
+
export { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
|
|
@@ -6,3 +6,5 @@ export { EventTopicBrokerDeleteTriggerResourceForPubSubSubscription } from './br
|
|
|
6
6
|
export { EventTopicBrokerDeleteTriggerResourceForServiceAccount } from './broker-delete-trigger-resource-service-account.js';
|
|
7
7
|
export { EventTopicBrokerGetTopicIdForPubSub } from './broker-get-topic-id-pubsub.js';
|
|
8
8
|
export { EventTopicBrokerPublishEventsForGoogle } from './broker-publish-events-google.js';
|
|
9
|
+
export { EventTopicCreateBackfillSourceForBigQuery } from './create-backfill-source-bigquery.js';
|
|
10
|
+
export { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WorkspaceContext } from '@causa/workspace';
|
|
2
|
+
import type { QueriedEvent } from '@causa/workspace-core';
|
|
3
|
+
import type { EventTopicQueryEventsForBigQuery } from './query-events-bigquery.js';
|
|
4
|
+
export default function call(this: EventTopicQueryEventsForBigQuery, context: WorkspaceContext): Promise<QueriedEvent[]>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BigQueryService } from '../../services/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The default duration applied to `from` when it is not set, in milliseconds.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_FROM_OFFSET = 60 * 60 * 1000;
|
|
6
|
+
/**
|
|
7
|
+
* The default value for `limit` when it is not set.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_LIMIT = 1000;
|
|
10
|
+
export default async function call(context) {
|
|
11
|
+
const from = this.from ?? new Date(Date.now() - DEFAULT_FROM_OFFSET);
|
|
12
|
+
const limit = this.limit ?? DEFAULT_LIMIT;
|
|
13
|
+
const googleConf = context.asConfiguration();
|
|
14
|
+
const projectId = googleConf.getOrThrow('google.project');
|
|
15
|
+
const rawEventsDatasetId = googleConf.getOrThrow('google.pubSub.bigQueryStorage.rawEventsDatasetId');
|
|
16
|
+
const tableName = this.topic.replace(/[-\.]/g, '_');
|
|
17
|
+
const tableId = `${projectId}.${rawEventsDatasetId}.${tableName}`;
|
|
18
|
+
const conditions = [`publish_time >= @from`];
|
|
19
|
+
const params = { from: from.toISOString() };
|
|
20
|
+
if (this.to) {
|
|
21
|
+
conditions.push(`publish_time < @to`);
|
|
22
|
+
params.to = this.to.toISOString();
|
|
23
|
+
}
|
|
24
|
+
if (this.filter) {
|
|
25
|
+
conditions.push(`(${this.filter})`);
|
|
26
|
+
}
|
|
27
|
+
const query = `
|
|
28
|
+
SELECT publish_time, data, attributes
|
|
29
|
+
FROM \`${tableId}\`
|
|
30
|
+
WHERE ${conditions.join(' AND ')}
|
|
31
|
+
ORDER BY publish_time ASC
|
|
32
|
+
LIMIT ${limit}`;
|
|
33
|
+
context.logger.debug(`Querying BigQuery for events:\n${query}`);
|
|
34
|
+
const bigQuery = context.service(BigQueryService).bigQuery;
|
|
35
|
+
const [rows] = await bigQuery.query({ query, params });
|
|
36
|
+
return rows.map((row) => ({
|
|
37
|
+
timestamp: new Date(row.publish_time.value),
|
|
38
|
+
attributes: row.attributes ? JSON.parse(row.attributes) : {},
|
|
39
|
+
data: JSON.parse(row.data.toString('utf8')),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WorkspaceContext } from '@causa/workspace';
|
|
2
|
+
import { EventTopicQueryEvents, type QueriedEvent } from '@causa/workspace-core';
|
|
3
|
+
/**
|
|
4
|
+
* Implements {@link EventTopicQueryEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
|
|
5
|
+
* subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`.
|
|
6
|
+
*/
|
|
7
|
+
export declare class EventTopicQueryEventsForBigQuery extends EventTopicQueryEvents {
|
|
8
|
+
_call(context: WorkspaceContext): Promise<QueriedEvent[]>;
|
|
9
|
+
_supports(context: WorkspaceContext): boolean;
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { callDeferred, WorkspaceContext } from '@causa/workspace';
|
|
2
|
+
import { EventTopicQueryEvents, } from '@causa/workspace-core';
|
|
3
|
+
/**
|
|
4
|
+
* Implements {@link EventTopicQueryEvents} for Pub/Sub topics whose events are stored in BigQuery via a BigQuery
|
|
5
|
+
* subscription. The events are read from the dataset configured at `google.pubSub.bigQueryStorage.rawEventsDatasetId`.
|
|
6
|
+
*/
|
|
7
|
+
export class EventTopicQueryEventsForBigQuery extends EventTopicQueryEvents {
|
|
8
|
+
async _call(context) {
|
|
9
|
+
return await callDeferred(this, context, import.meta.url);
|
|
10
|
+
}
|
|
11
|
+
_supports(context) {
|
|
12
|
+
const conf = context.asConfiguration();
|
|
13
|
+
return (conf.get('events.broker') === 'google.pubSub' &&
|
|
14
|
+
conf.get('events.format') === 'json' &&
|
|
15
|
+
!!conf.get('google.pubSub.bigQueryStorage.rawEventsDatasetId'));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { deleteApp, initializeApp } from 'firebase-admin/app';
|
|
2
|
+
import { DocumentReference, GeoPoint, getFirestore, Timestamp, } from 'firebase-admin/firestore';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
export default async function call(context) {
|
|
5
|
+
if (!this.query) {
|
|
6
|
+
throw new Error(`The 'query' input is required for the 'google.firestore' engine.`);
|
|
7
|
+
}
|
|
8
|
+
const googleConf = context.asConfiguration();
|
|
9
|
+
const projectId = googleConf.getOrThrow('google.project');
|
|
10
|
+
const database = this.database ?? '(default)';
|
|
11
|
+
context.logger.debug(`🗃️ Fetching Firestore document '${this.query}' from database '${database}' in project '${projectId}'.`);
|
|
12
|
+
const app = initializeApp({ projectId }, randomUUID());
|
|
13
|
+
try {
|
|
14
|
+
const firestore = getFirestore(app, database);
|
|
15
|
+
const snapshot = await firestore.doc(this.query).get();
|
|
16
|
+
return snapshot.exists ? [toJson(snapshot.data())] : [];
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await deleteApp(app);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Recursively converts Firestore-specific types to JSON-friendly equivalents:
|
|
24
|
+
* - {@link Timestamp} → {@link Date}
|
|
25
|
+
* - {@link GeoPoint} → `{ latitude, longitude }`
|
|
26
|
+
* - {@link DocumentReference} → its `path`
|
|
27
|
+
*
|
|
28
|
+
* Other values (including `Buffer`) are returned as-is.
|
|
29
|
+
*/
|
|
30
|
+
function toJson(value) {
|
|
31
|
+
if (value === null || typeof value !== 'object') {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (value instanceof Timestamp) {
|
|
35
|
+
return value.toDate();
|
|
36
|
+
}
|
|
37
|
+
if (value instanceof GeoPoint) {
|
|
38
|
+
return { latitude: value.latitude, longitude: value.longitude };
|
|
39
|
+
}
|
|
40
|
+
if (value instanceof DocumentReference) {
|
|
41
|
+
return value.path;
|
|
42
|
+
}
|
|
43
|
+
if (Buffer.isBuffer(value)) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value.map(toJson);
|
|
48
|
+
}
|
|
49
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toJson(v)]));
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { WorkspaceContext } from '@causa/workspace';
|
|
2
|
+
import { DatabaseQueryRecords } from '@causa/workspace-core';
|
|
3
|
+
/**
|
|
4
|
+
* The engine identifier handled by this implementation.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FIRESTORE_ENGINE = "google.firestore";
|
|
7
|
+
/**
|
|
8
|
+
* Implements {@link DatabaseQueryRecords} for Firestore.
|
|
9
|
+
*
|
|
10
|
+
* The `database` input is optional and defaults to the default Firestore database. The `query` input is the path to a
|
|
11
|
+
* single document (e.g. `users/abc`).
|
|
12
|
+
*
|
|
13
|
+
* Returns an array containing a single document's data, or an empty array if the document does not exist.
|
|
14
|
+
*/
|
|
15
|
+
export declare class DatabaseQueryRecordsForFirestore extends DatabaseQueryRecords {
|
|
16
|
+
_call(context: WorkspaceContext): Promise<any[]>;
|
|
17
|
+
_supports(): boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { callDeferred, WorkspaceContext } from '@causa/workspace';
|
|
2
|
+
import { DatabaseQueryRecords } from '@causa/workspace-core';
|
|
3
|
+
/**
|
|
4
|
+
* The engine identifier handled by this implementation.
|
|
5
|
+
*/
|
|
6
|
+
export const FIRESTORE_ENGINE = 'google.firestore';
|
|
7
|
+
/**
|
|
8
|
+
* Implements {@link DatabaseQueryRecords} for Firestore.
|
|
9
|
+
*
|
|
10
|
+
* The `database` input is optional and defaults to the default Firestore database. The `query` input is the path to a
|
|
11
|
+
* single document (e.g. `users/abc`).
|
|
12
|
+
*
|
|
13
|
+
* Returns an array containing a single document's data, or an empty array if the document does not exist.
|
|
14
|
+
*/
|
|
15
|
+
export class DatabaseQueryRecordsForFirestore extends DatabaseQueryRecords {
|
|
16
|
+
async _call(context) {
|
|
17
|
+
return await callDeferred(this, context, import.meta.url);
|
|
18
|
+
}
|
|
19
|
+
_supports() {
|
|
20
|
+
return this.engine === FIRESTORE_ENGINE;
|
|
21
|
+
}
|
|
22
|
+
}
|